diff --git a/.gitignore b/.gitignore
index a547bf36d..eb7d123fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+/docs
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
index d9ae6b1fb..cdd6f7227 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,9 +1,9 @@
{
- "semi": false,
+ "semi": true,
"printWidth": 120,
"tabWidth": 2,
"singleQuote": false,
"quoteProps": "consistent",
"trailingComma": "all",
"singleAttributePerLine": false
-}
\ No newline at end of file
+}
diff --git a/eslint.config.js b/eslint.config.js
index 092408a9f..9aa5c7df5 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,27 +1,56 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
+import js from "@eslint/js"
+import globals from "globals"
+import reactHooks from "eslint-plugin-react-hooks"
+import reactRefresh from "eslint-plugin-react-refresh"
+import tseslint from "typescript-eslint"
+import pluginImport from "eslint-plugin-import"
+import path from "node:path"
export default tseslint.config(
- { ignores: ['dist'] },
+ { ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
- files: ['**/*.{ts,tsx}'],
+ files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
+ parserOptions: {
+ project: ["./tsconfig.app.json", "./tsconfig.node.json"],
+ },
},
plugins: {
- 'react-hooks': reactHooks,
- 'react-refresh': reactRefresh,
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
+ "import": pluginImport,
+ },
+ settings: {
+ "import/resolver": {
+ typescript: {
+ project: path.resolve(process.cwd(), "tsconfig.app.json"),
+ },
+ },
},
rules: {
...reactHooks.configs.recommended.rules,
- 'react-refresh/only-export-components': [
- 'warn',
- { allowConstantExport: true },
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
+ "import/no-unresolved": "error",
+ "import/order": [
+ "warn",
+ {
+ "groups": ["builtin", "external", "internal", ["parent", "sibling", "index"], "object", "type"],
+ "pathGroups": [
+ { pattern: "@shared/**", group: "internal", position: "before" },
+ { pattern: "@entities/**", group: "internal", position: "before" },
+ { pattern: "@features/**", group: "internal", position: "before" },
+ { pattern: "@widgets/**", group: "internal", position: "before" },
+ { pattern: "@pages/**", group: "internal", position: "before" },
+ { pattern: "@app/**", group: "internal", position: "before" },
+ { pattern: "@/**", group: "internal", position: "before" },
+ ],
+ "pathGroupsExcludedImportTypes": ["builtin"],
+ "alphabetize": { order: "asc", caseInsensitive: true },
+ "newlines-between": "always",
+ },
],
},
},
diff --git a/package.json b/package.json
index e014c5272..240fc3d00 100644
--- a/package.json
+++ b/package.json
@@ -7,11 +7,15 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
+ "deploy": "gh-pages -d dist -b gh-pages",
"preview": "vite preview",
"test": "vitest",
"coverage": "vitest run --coverage"
},
"dependencies": {
+ "@tanstack/react-query": "5.85.3",
+ "@tanstack/react-query-devtools": "5.85.3",
+ "jotai": "2.13.1",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
@@ -28,9 +32,12 @@
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"eslint": "^9.33.0",
+ "eslint-import-resolver-typescript": "4.4.4",
+ "eslint-plugin-import": "2.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
+ "gh-pages": "6.3.0",
"jsdom": "^26.1.0",
"lucide-react": "^0.539.0",
"msw": "^2.10.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6b2a40d18..8a3b108a3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,15 @@ importers:
.:
dependencies:
+ '@tanstack/react-query':
+ specifier: 5.85.3
+ version: 5.85.3(react@19.1.1)
+ '@tanstack/react-query-devtools':
+ specifier: 5.85.3
+ version: 5.85.3(@tanstack/react-query@5.85.3(react@19.1.1))(react@19.1.1)
+ jotai:
+ specifier: 2.13.1
+ version: 2.13.1(@babel/core@7.28.0)(@babel/template@7.27.2)(@types/react@19.1.9)(react@19.1.1)
react:
specifier: ^19.1.1
version: 19.1.1
@@ -51,12 +60,21 @@ importers:
eslint:
specifier: ^9.33.0
version: 9.33.0
+ eslint-import-resolver-typescript:
+ specifier: 4.4.4
+ version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.33.0)
+ eslint-plugin-import:
+ specifier: 2.32.0
+ version: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0)(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.33.0)
eslint-plugin-react-hooks:
specifier: ^5.2.0
version: 5.2.0(eslint@9.33.0)
eslint-plugin-react-refresh:
specifier: ^0.4.20
version: 0.4.20(eslint@9.33.0)
+ gh-pages:
+ specifier: 6.3.0
+ version: 6.3.0
globals:
specifier: ^16.3.0
version: 16.3.0
@@ -103,10 +121,6 @@ packages:
'@asamuzakjp/css-color@2.8.3':
resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==}
- '@babel/code-frame@7.26.2':
- resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
- engines: {node: '>=6.9.0'}
-
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@@ -248,6 +262,15 @@ packages:
resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
engines: {node: '>=18'}
+ '@emnapi/core@1.4.5':
+ resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
+
+ '@emnapi/runtime@1.4.5':
+ resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
+
+ '@emnapi/wasi-threads@1.0.4':
+ resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==}
+
'@esbuild/aix-ppc64@0.25.3':
resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==}
engines: {node: '>=18'}
@@ -525,6 +548,9 @@ packages:
resolution: {integrity: sha512-B9nHSJYtsv79uo7QdkZ/b/WoKm20IkVSmTc/WCKarmDtFwM0dRx2ouEniqwNkzCSLn3fydzKmnMzjtfdOWt3VQ==}
engines: {node: '>=18'}
+ '@napi-rs/wasm-runtime@0.2.12':
+ resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -930,6 +956,26 @@ packages:
cpu: [x64]
os: [win32]
+ '@rtsao/scc@1.1.0':
+ resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+
+ '@tanstack/query-core@5.85.3':
+ resolution: {integrity: sha512-9Ne4USX83nHmRuEYs78LW+3lFEEO2hBDHu7mrdIgAFx5Zcrs7ker3n/i8p4kf6OgKExmaDN5oR0efRD7i2J0DQ==}
+
+ '@tanstack/query-devtools@5.84.0':
+ resolution: {integrity: sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==}
+
+ '@tanstack/react-query-devtools@5.85.3':
+ resolution: {integrity: sha512-WSVweCE1Kh1BVvPDHAmLgGT+GGTJQ9+a7bVqzD+zUiUTht+salJjYm5nikpMNaHFPJV102TCYdvgHgBXtURRNg==}
+ peerDependencies:
+ '@tanstack/react-query': ^5.85.3
+ react: ^18 || ^19
+
+ '@tanstack/react-query@5.85.3':
+ resolution: {integrity: sha512-AqU8TvNh5GVIE8I+TUU0noryBRy7gOY0XhSayVXmOPll4UkZeLWKDwi0rtWOZbwLRCbyxorfJ5DIjDqE7GXpcQ==}
+ peerDependencies:
+ react: ^18 || ^19
+
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@@ -959,6 +1005,9 @@ packages:
peerDependencies:
'@testing-library/dom': '>=7.21.4'
+ '@tybys/wasm-util@0.10.0':
+ resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
+
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@@ -992,6 +1041,9 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+ '@types/json5@0.0.29':
+ resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
+
'@types/node@22.8.1':
resolution: {integrity: sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==}
@@ -1068,6 +1120,101 @@ packages:
resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@unrs/resolver-binding-android-arm-eabi@1.11.1':
+ resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
+ cpu: [arm]
+ os: [android]
+
+ '@unrs/resolver-binding-android-arm64@1.11.1':
+ resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==}
+ cpu: [arm64]
+ os: [android]
+
+ '@unrs/resolver-binding-darwin-arm64@1.11.1':
+ resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@unrs/resolver-binding-darwin-x64@1.11.1':
+ resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@unrs/resolver-binding-freebsd-x64@1.11.1':
+ resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1':
+ resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1':
+ resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-arm64-gnu@1.11.1':
+ resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
+ resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
+ resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
+ resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
+ resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
+ resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
+ resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
+ cpu: [x64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-x64-musl@1.11.1':
+ resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@unrs/resolver-binding-wasm32-wasi@1.11.1':
+ resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@unrs/resolver-binding-win32-arm64-msvc@1.11.1':
+ resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@unrs/resolver-binding-win32-ia32-msvc@1.11.1':
+ resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
+ resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==}
+ cpu: [x64]
+ os: [win32]
+
'@vitejs/plugin-react@5.0.0':
resolution: {integrity: sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1179,13 +1326,52 @@ packages:
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+ array-buffer-byte-length@1.0.2:
+ resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
+ engines: {node: '>= 0.4'}
+
+ array-includes@3.1.9:
+ resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
+ engines: {node: '>= 0.4'}
+
+ array-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+
+ array.prototype.findlastindex@1.2.6:
+ resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flat@1.3.3:
+ resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flatmap@1.3.3:
+ resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==}
+ engines: {node: '>= 0.4'}
+
+ arraybuffer.prototype.slice@1.0.4:
+ resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
+ engines: {node: '>= 0.4'}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ async-function@1.0.0:
+ resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
+ engines: {node: '>= 0.4'}
+
+ async@3.2.6:
+ resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
+
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+ available-typed-arrays@1.0.7:
+ resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+ engines: {node: '>= 0.4'}
+
axios@1.11.0:
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
@@ -1215,6 +1401,14 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
+ call-bind@1.0.8:
+ resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -1260,6 +1454,13 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
+ commander@13.1.0:
+ resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
+ engines: {node: '>=18'}
+
+ commondir@1.0.1:
+ resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1292,6 +1493,26 @@ packages:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
+ data-view-buffer@1.0.2:
+ resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-length@1.0.2:
+ resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-offset@1.0.1:
+ resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
+ engines: {node: '>= 0.4'}
+
+ debug@3.2.7:
+ resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
@@ -1320,6 +1541,14 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+ engines: {node: '>= 0.4'}
+
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
@@ -1331,6 +1560,14 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+ dir-glob@3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+
+ doctrine@2.1.0:
+ resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
+ engines: {node: '>=0.10.0'}
+
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
@@ -1344,6 +1581,9 @@ packages:
electron-to-chromium@1.5.45:
resolution: {integrity: sha512-vOzZS6uZwhhbkZbcRyiy99Wg+pYFV5hk+5YaECvx0+Z31NR3Tt5zS6dze2OepT6PCTzVzT0dIJItti+uAW5zmw==}
+ email-addresses@5.0.0:
+ resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==}
+
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -1351,6 +1591,10 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
+ es-abstract@1.24.0:
+ resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==}
+ engines: {node: '>= 0.4'}
+
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -1370,6 +1614,14 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
+ es-shim-unscopables@1.1.0:
+ resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==}
+ engines: {node: '>= 0.4'}
+
+ es-to-primitive@1.3.0:
+ resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
+ engines: {node: '>= 0.4'}
+
esbuild@0.25.3:
resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
engines: {node: '>=18'}
@@ -1379,10 +1631,70 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
+ escape-string-regexp@1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
+ eslint-import-context@0.1.9:
+ resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ unrs-resolver: ^1.0.0
+ peerDependenciesMeta:
+ unrs-resolver:
+ optional: true
+
+ eslint-import-resolver-node@0.3.9:
+ resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
+
+ eslint-import-resolver-typescript@4.4.4:
+ resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==}
+ engines: {node: ^16.17.0 || >=18.6.0}
+ peerDependencies:
+ eslint: '*'
+ eslint-plugin-import: '*'
+ eslint-plugin-import-x: '*'
+ peerDependenciesMeta:
+ eslint-plugin-import:
+ optional: true
+ eslint-plugin-import-x:
+ optional: true
+
+ eslint-module-utils@2.12.1:
+ resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@typescript-eslint/parser': '*'
+ eslint: '*'
+ eslint-import-resolver-node: '*'
+ eslint-import-resolver-typescript: '*'
+ eslint-import-resolver-webpack: '*'
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+ eslint:
+ optional: true
+ eslint-import-resolver-node:
+ optional: true
+ eslint-import-resolver-typescript:
+ optional: true
+ eslint-import-resolver-webpack:
+ optional: true
+
+ eslint-plugin-import@2.32.0:
+ resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@typescript-eslint/parser': '*'
+ eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+
eslint-plugin-react-hooks@5.2.0:
resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
engines: {node: '>=10'}
@@ -1471,10 +1783,26 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
+ filename-reserved-regex@2.0.0:
+ resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==}
+ engines: {node: '>=4'}
+
+ filenamify@4.3.0:
+ resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==}
+ engines: {node: '>=8'}
+
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ find-cache-dir@3.3.2:
+ resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
+ engines: {node: '>=8'}
+
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -1495,10 +1823,18 @@ packages:
debug:
optional: true
+ for-each@0.3.5:
+ resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+ engines: {node: '>= 0.4'}
+
form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
+ fs-extra@11.3.1:
+ resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==}
+ engines: {node: '>=14.14'}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1507,6 +1843,13 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+ function.prototype.name@1.1.8:
+ resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
+ engines: {node: '>= 0.4'}
+
+ functions-have-names@1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -1527,6 +1870,18 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
+ get-symbol-description@1.1.0:
+ resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
+ engines: {node: '>= 0.4'}
+
+ get-tsconfig@4.10.1:
+ resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
+
+ gh-pages@6.3.0:
+ resolution: {integrity: sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -1543,10 +1898,21 @@ packages:
resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==}
engines: {node: '>=18'}
+ globalthis@1.0.4:
+ resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
+ engines: {node: '>= 0.4'}
+
+ globby@11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
@@ -1554,10 +1920,21 @@ packages:
resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
+ has-bigints@1.1.0:
+ resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
+ engines: {node: '>= 0.4'}
+
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-proto@1.2.0:
+ resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==}
+ engines: {node: '>= 0.4'}
+
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
@@ -1609,21 +1986,80 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
+ internal-slot@1.1.0:
+ resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
+ engines: {node: '>= 0.4'}
+
+ is-array-buffer@3.0.5:
+ resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
+ engines: {node: '>= 0.4'}
+
+ is-async-function@2.1.1:
+ resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
+ engines: {node: '>= 0.4'}
+
+ is-bigint@1.1.0:
+ resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
+ engines: {node: '>= 0.4'}
+
+ is-boolean-object@1.2.2:
+ resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
+ engines: {node: '>= 0.4'}
+
+ is-bun-module@2.0.0:
+ resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==}
+
+ is-callable@1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+
+ is-core-module@2.16.1:
+ resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
+ engines: {node: '>= 0.4'}
+
+ is-data-view@1.0.2:
+ resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==}
+ engines: {node: '>= 0.4'}
+
+ is-date-object@1.1.0:
+ resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+ engines: {node: '>= 0.4'}
+
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
+ is-finalizationregistry@1.1.1:
+ resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
+ engines: {node: '>= 0.4'}
+
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
+ is-generator-function@1.1.0:
+ resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
+ engines: {node: '>= 0.4'}
+
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
+ is-map@2.0.3:
+ resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
+ engines: {node: '>= 0.4'}
+
+ is-negative-zero@2.0.3:
+ resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
+ engines: {node: '>= 0.4'}
+
is-node-process@1.2.0:
resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
+ is-number-object@1.1.1:
+ resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
+ engines: {node: '>= 0.4'}
+
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
@@ -1631,9 +2067,66 @@ packages:
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
+ is-set@2.0.3:
+ resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
+ engines: {node: '>= 0.4'}
+
+ is-shared-array-buffer@1.0.4:
+ resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
+ engines: {node: '>= 0.4'}
+
+ is-string@1.1.1:
+ resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
+ engines: {node: '>= 0.4'}
+
+ is-symbol@1.1.1:
+ resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
+ engines: {node: '>= 0.4'}
+
+ is-typed-array@1.1.15:
+ resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
+ engines: {node: '>= 0.4'}
+
+ is-weakmap@2.0.2:
+ resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
+ engines: {node: '>= 0.4'}
+
+ is-weakref@1.1.1:
+ resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==}
+ engines: {node: '>= 0.4'}
+
+ is-weakset@2.0.4:
+ resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
+ engines: {node: '>= 0.4'}
+
+ isarray@2.0.5:
+ resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ jotai@2.13.1:
+ resolution: {integrity: sha512-cRsw6kFeGC9Z/D3egVKrTXRweycZ4z/k7i2MrfCzPYsL9SIWcPXTyqv258/+Ay8VUEcihNiE/coBLE6Kic6b8A==}
+ 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
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1667,11 +2160,18 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+ json5@1.0.2:
+ resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
+ hasBin: true
+
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
+ jsonfile@6.2.0:
+ resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -1679,6 +2179,10 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -1713,6 +2217,10 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
+ make-dir@3.1.0:
+ resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
+ engines: {node: '>=8'}
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -1744,6 +2252,9 @@ packages:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
mrmime@2.0.0:
resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
engines: {node: '>=10'}
@@ -1770,6 +2281,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ napi-postinstall@0.3.3:
+ resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ hasBin: true
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -1779,21 +2295,61 @@ packages:
nwsapi@2.2.16:
resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==}
- optionator@0.9.4:
- resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
- engines: {node: '>= 0.8.0'}
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
- outvariant@1.4.3:
- resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+ object-keys@1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
- p-limit@3.1.0:
- resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ object.assign@4.1.7:
+ resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
+ engines: {node: '>= 0.4'}
+
+ object.fromentries@2.0.8:
+ resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==}
+ engines: {node: '>= 0.4'}
+
+ object.groupby@1.0.3:
+ resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==}
+ engines: {node: '>= 0.4'}
+
+ object.values@1.2.1:
+ resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
+ engines: {node: '>= 0.4'}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ outvariant@1.4.3:
+ resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+
+ own-keys@1.0.1:
+ resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
+ engines: {node: '>= 0.4'}
+
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -1809,9 +2365,16 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -1834,6 +2397,14 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
+ pkg-dir@4.2.0:
+ resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
+ engines: {node: '>=8'}
+
+ possible-typed-array-names@1.1.0:
+ resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+ engines: {node: '>= 0.4'}
+
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -1934,9 +2505,17 @@ packages:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
+ reflect.getprototypeof@1.0.10:
+ resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
+ engines: {node: '>= 0.4'}
+
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+ regexp.prototype.flags@1.5.4:
+ resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+ engines: {node: '>= 0.4'}
+
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@@ -1948,6 +2527,14 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ resolve@1.22.10:
+ resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -1963,6 +2550,18 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ safe-array-concat@1.1.3:
+ resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
+ engines: {node: '>=0.4'}
+
+ safe-push-apply@1.0.0:
+ resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
+ engines: {node: '>= 0.4'}
+
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -1982,9 +2581,26 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
set-cookie-parser@2.7.1:
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-function-name@2.0.2:
+ resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+ engines: {node: '>= 0.4'}
+
+ set-proto@1.0.0:
+ resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
+ engines: {node: '>= 0.4'}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -1993,6 +2609,22 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -2004,10 +2636,18 @@ packages:
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
engines: {node: '>= 10'}
+ slash@3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ stable-hash-x@0.2.0:
+ resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
+ engines: {node: '>=12.0.0'}
+
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -2018,6 +2658,10 @@ packages:
std-env@3.9.0:
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
+ stop-iteration-iterator@1.1.0:
+ resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
+ engines: {node: '>= 0.4'}
+
strict-event-emitter@0.5.1:
resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
@@ -2025,10 +2669,26 @@ packages:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
+ string.prototype.trim@1.2.10:
+ resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimend@1.0.9:
+ resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimstart@1.0.8:
+ resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
+ engines: {node: '>= 0.4'}
+
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
+ strip-bom@3.0.0:
+ resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+ engines: {node: '>=4'}
+
strip-indent@3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
@@ -2040,10 +2700,18 @@ packages:
strip-literal@3.0.0:
resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
+ strip-outer@1.0.1:
+ resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==}
+ engines: {node: '>=0.10.0'}
+
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@@ -2100,12 +2768,19 @@ packages:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
+ trim-repeated@1.0.0:
+ resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==}
+ engines: {node: '>=0.10.0'}
+
ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
+ tsconfig-paths@3.15.0:
+ resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
+
tslib@2.8.0:
resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==}
@@ -2121,6 +2796,22 @@ packages:
resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==}
engines: {node: '>=16'}
+ typed-array-buffer@1.0.3:
+ resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-length@1.0.3:
+ resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-offset@1.0.4:
+ resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-length@1.0.7:
+ resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
+ engines: {node: '>= 0.4'}
+
typescript-eslint@8.39.0:
resolution: {integrity: sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2133,6 +2824,10 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ unbox-primitive@1.1.0:
+ resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
+ engines: {node: '>= 0.4'}
+
undici-types@6.19.8:
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
@@ -2140,6 +2835,13 @@ packages:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
+ unrs-resolver@1.11.1:
+ resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
+
update-browserslist-db@1.1.1:
resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
hasBin: true
@@ -2281,6 +2983,22 @@ packages:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
+ which-boxed-primitive@1.1.1:
+ resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
+ engines: {node: '>= 0.4'}
+
+ which-builtin-type@1.2.1:
+ resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==}
+ engines: {node: '>= 0.4'}
+
+ which-collection@1.0.2:
+ resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
+ engines: {node: '>= 0.4'}
+
+ which-typed-array@1.1.19:
+ resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
+ engines: {node: '>= 0.4'}
+
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -2362,12 +3080,6 @@ snapshots:
'@csstools/css-tokenizer': 3.0.3
lru-cache: 10.4.3
- '@babel/code-frame@7.26.2':
- dependencies:
- '@babel/helper-validator-identifier': 7.25.9
- js-tokens: 4.0.0
- picocolors: 1.1.1
-
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
@@ -2530,6 +3242,22 @@ snapshots:
'@csstools/css-tokenizer@3.0.3': {}
+ '@emnapi/core@1.4.5':
+ dependencies:
+ '@emnapi/wasi-threads': 1.0.4
+ tslib: 2.8.0
+ optional: true
+
+ '@emnapi/runtime@1.4.5':
+ dependencies:
+ tslib: 2.8.0
+ optional: true
+
+ '@emnapi/wasi-threads@1.0.4':
+ dependencies:
+ tslib: 2.8.0
+ optional: true
+
'@esbuild/aix-ppc64@0.25.3':
optional: true
@@ -2746,6 +3474,13 @@ snapshots:
outvariant: 1.4.3
strict-event-emitter: 0.5.1
+ '@napi-rs/wasm-runtime@0.2.12':
+ dependencies:
+ '@emnapi/core': 1.4.5
+ '@emnapi/runtime': 1.4.5
+ '@tybys/wasm-util': 0.10.0
+ optional: true
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -3081,9 +3816,26 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.46.2':
optional: true
+ '@rtsao/scc@1.1.0': {}
+
+ '@tanstack/query-core@5.85.3': {}
+
+ '@tanstack/query-devtools@5.84.0': {}
+
+ '@tanstack/react-query-devtools@5.85.3(@tanstack/react-query@5.85.3(react@19.1.1))(react@19.1.1)':
+ dependencies:
+ '@tanstack/query-devtools': 5.84.0
+ '@tanstack/react-query': 5.85.3(react@19.1.1)
+ react: 19.1.1
+
+ '@tanstack/react-query@5.85.3(react@19.1.1)':
+ dependencies:
+ '@tanstack/query-core': 5.85.3
+ react: 19.1.1
+
'@testing-library/dom@10.4.0':
dependencies:
- '@babel/code-frame': 7.26.2
+ '@babel/code-frame': 7.27.1
'@babel/runtime': 7.26.0
'@types/aria-query': 5.0.4
aria-query: 5.3.0
@@ -3116,6 +3868,11 @@ snapshots:
dependencies:
'@testing-library/dom': 10.4.0
+ '@tybys/wasm-util@0.10.0':
+ dependencies:
+ tslib: 2.8.0
+ optional: true
+
'@types/aria-query@5.0.4': {}
'@types/babel__core@7.20.5':
@@ -3153,6 +3910,8 @@ snapshots:
'@types/json-schema@7.0.15': {}
+ '@types/json5@0.0.29': {}
+
'@types/node@22.8.1':
dependencies:
undici-types: 6.19.8
@@ -3262,6 +4021,65 @@ snapshots:
'@typescript-eslint/types': 8.39.0
eslint-visitor-keys: 4.2.1
+ '@unrs/resolver-binding-android-arm-eabi@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-android-arm64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-darwin-arm64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-darwin-x64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-freebsd-x64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-x64-musl@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-wasm32-wasi@1.11.1':
+ dependencies:
+ '@napi-rs/wasm-runtime': 0.2.12
+ optional: true
+
+ '@unrs/resolver-binding-win32-arm64-msvc@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-win32-ia32-msvc@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
+ optional: true
+
'@vitejs/plugin-react@5.0.0(vite@7.1.1(@types/node@22.8.1))':
dependencies:
'@babel/core': 7.28.0
@@ -3347,7 +4165,7 @@ snapshots:
'@vitest/utils@2.1.3':
dependencies:
'@vitest/pretty-format': 2.1.3
- loupe: 3.1.3
+ loupe: 3.2.0
tinyrainbow: 1.2.0
'@vitest/utils@3.2.4':
@@ -3393,10 +4211,70 @@ snapshots:
dependencies:
dequal: 2.0.3
+ array-buffer-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ is-array-buffer: 3.0.5
+
+ array-includes@3.1.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ is-string: 1.1.1
+ math-intrinsics: 1.1.0
+
+ array-union@2.1.0: {}
+
+ array.prototype.findlastindex@1.2.6:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flat@1.3.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flatmap@1.3.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-shim-unscopables: 1.1.0
+
+ arraybuffer.prototype.slice@1.0.4:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ is-array-buffer: 3.0.5
+
assertion-error@2.0.1: {}
+ async-function@1.0.0: {}
+
+ async@3.2.6: {}
+
asynckit@0.4.0: {}
+ available-typed-arrays@1.0.7:
+ dependencies:
+ possible-typed-array-names: 1.1.0
+
axios@1.11.0:
dependencies:
follow-redirects: 1.15.9
@@ -3434,6 +4312,18 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
+ call-bind@1.0.8:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ get-intrinsic: 1.3.0
+ set-function-length: 1.2.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
callsites@3.1.0: {}
caniuse-lite@1.0.30001669: {}
@@ -3477,6 +4367,10 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
+ commander@13.1.0: {}
+
+ commondir@1.0.1: {}
+
concat-map@0.0.1: {}
convert-source-map@2.0.0: {}
@@ -3505,6 +4399,28 @@ snapshots:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
+ data-view-buffer@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-offset@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ debug@3.2.7:
+ dependencies:
+ ms: 2.1.3
+
debug@4.4.0:
dependencies:
ms: 2.1.3
@@ -3519,12 +4435,32 @@ snapshots:
deep-is@0.1.4: {}
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ define-properties@1.2.1:
+ dependencies:
+ define-data-property: 1.1.4
+ has-property-descriptors: 1.0.2
+ object-keys: 1.1.1
+
delayed-stream@1.0.0: {}
dequal@2.0.3: {}
detect-node-es@1.1.0: {}
+ dir-glob@3.0.1:
+ dependencies:
+ path-type: 4.0.0
+
+ doctrine@2.1.0:
+ dependencies:
+ esutils: 2.0.3
+
dom-accessibility-api@0.5.16: {}
dom-accessibility-api@0.6.3: {}
@@ -3537,10 +4473,69 @@ snapshots:
electron-to-chromium@1.5.45: {}
+ email-addresses@5.0.0: {}
+
emoji-regex@8.0.0: {}
entities@4.5.0: {}
+ es-abstract@1.24.0:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ arraybuffer.prototype.slice: 1.0.4
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ data-view-buffer: 1.0.2
+ data-view-byte-length: 1.0.2
+ data-view-byte-offset: 1.0.1
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-set-tostringtag: 2.1.0
+ es-to-primitive: 1.3.0
+ function.prototype.name: 1.1.8
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ get-symbol-description: 1.1.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ internal-slot: 1.1.0
+ is-array-buffer: 3.0.5
+ is-callable: 1.2.7
+ is-data-view: 1.0.2
+ is-negative-zero: 2.0.3
+ is-regex: 1.2.1
+ is-set: 2.0.3
+ is-shared-array-buffer: 1.0.4
+ is-string: 1.1.1
+ is-typed-array: 1.1.15
+ is-weakref: 1.1.1
+ math-intrinsics: 1.1.0
+ object-inspect: 1.13.4
+ object-keys: 1.1.1
+ object.assign: 4.1.7
+ own-keys: 1.0.1
+ regexp.prototype.flags: 1.5.4
+ safe-array-concat: 1.1.3
+ safe-push-apply: 1.0.0
+ safe-regex-test: 1.1.0
+ set-proto: 1.0.0
+ stop-iteration-iterator: 1.1.0
+ string.prototype.trim: 1.2.10
+ string.prototype.trimend: 1.0.9
+ string.prototype.trimstart: 1.0.8
+ typed-array-buffer: 1.0.3
+ typed-array-byte-length: 1.0.3
+ typed-array-byte-offset: 1.0.4
+ typed-array-length: 1.0.7
+ unbox-primitive: 1.1.0
+ which-typed-array: 1.1.19
+
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -3558,6 +4553,16 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
+ es-shim-unscopables@1.1.0:
+ dependencies:
+ hasown: 2.0.2
+
+ es-to-primitive@1.3.0:
+ dependencies:
+ is-callable: 1.2.7
+ is-date-object: 1.1.0
+ is-symbol: 1.1.1
+
esbuild@0.25.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.3
@@ -3588,8 +4593,80 @@ snapshots:
escalade@3.2.0: {}
+ escape-string-regexp@1.0.5: {}
+
escape-string-regexp@4.0.0: {}
+ eslint-import-context@0.1.9(unrs-resolver@1.11.1):
+ dependencies:
+ get-tsconfig: 4.10.1
+ stable-hash-x: 0.2.0
+ optionalDependencies:
+ unrs-resolver: 1.11.1
+
+ eslint-import-resolver-node@0.3.9:
+ dependencies:
+ debug: 3.2.7
+ is-core-module: 2.16.1
+ resolve: 1.22.10
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.33.0):
+ dependencies:
+ debug: 4.4.1
+ eslint: 9.33.0
+ eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
+ get-tsconfig: 4.10.1
+ is-bun-module: 2.0.0
+ stable-hash-x: 0.2.0
+ tinyglobby: 0.2.14
+ unrs-resolver: 1.11.1
+ optionalDependencies:
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0)(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.33.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.33.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.33.0):
+ dependencies:
+ debug: 3.2.7
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.39.0(eslint@9.33.0)(typescript@5.9.2)
+ eslint: 9.33.0
+ eslint-import-resolver-node: 0.3.9
+ eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.33.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0)(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.33.0):
+ dependencies:
+ '@rtsao/scc': 1.1.0
+ array-includes: 3.1.9
+ array.prototype.findlastindex: 1.2.6
+ array.prototype.flat: 1.3.3
+ array.prototype.flatmap: 1.3.3
+ debug: 3.2.7
+ doctrine: 2.1.0
+ eslint: 9.33.0
+ eslint-import-resolver-node: 0.3.9
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.39.0(eslint@9.33.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.33.0)
+ hasown: 2.0.2
+ is-core-module: 2.16.1
+ is-glob: 4.0.3
+ minimatch: 3.1.2
+ object.fromentries: 2.0.8
+ object.groupby: 1.0.3
+ object.values: 1.2.1
+ semver: 6.3.1
+ string.prototype.trimend: 1.0.9
+ tsconfig-paths: 3.15.0
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.39.0(eslint@9.33.0)(typescript@5.9.2)
+ transitivePeerDependencies:
+ - eslint-import-resolver-typescript
+ - eslint-import-resolver-webpack
+ - supports-color
+
eslint-plugin-react-hooks@5.2.0(eslint@9.33.0):
dependencies:
eslint: 9.33.0
@@ -3697,10 +4774,29 @@ snapshots:
dependencies:
flat-cache: 4.0.1
+ filename-reserved-regex@2.0.0: {}
+
+ filenamify@4.3.0:
+ dependencies:
+ filename-reserved-regex: 2.0.0
+ strip-outer: 1.0.1
+ trim-repeated: 1.0.0
+
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
+ find-cache-dir@3.3.2:
+ dependencies:
+ commondir: 1.0.1
+ make-dir: 3.1.0
+ pkg-dir: 4.2.0
+
+ find-up@4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -3715,6 +4811,10 @@ snapshots:
follow-redirects@1.15.9: {}
+ for-each@0.3.5:
+ dependencies:
+ is-callable: 1.2.7
+
form-data@4.0.4:
dependencies:
asynckit: 0.4.0
@@ -3723,11 +4823,28 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
+ fs-extra@11.3.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.0
+ universalify: 2.0.1
+
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
+ function.prototype.name@1.1.8:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ functions-have-names: 1.2.3
+ hasown: 2.0.2
+ is-callable: 1.2.7
+
+ functions-have-names@1.2.3: {}
+
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
@@ -3752,6 +4869,26 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
+ get-symbol-description@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+
+ get-tsconfig@4.10.1:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ gh-pages@6.3.0:
+ dependencies:
+ async: 3.2.6
+ commander: 13.1.0
+ email-addresses: 5.0.0
+ filenamify: 4.3.0
+ find-cache-dir: 3.3.2
+ fs-extra: 11.3.1
+ globby: 11.1.0
+
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -3764,14 +4901,40 @@ snapshots:
globals@16.3.0: {}
+ globalthis@1.0.4:
+ dependencies:
+ define-properties: 1.2.1
+ gopd: 1.2.0
+
+ globby@11.1.0:
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.3.2
+ ignore: 5.3.2
+ merge2: 1.4.1
+ slash: 3.0.0
+
gopd@1.2.0: {}
+ graceful-fs@4.2.11: {}
+
graphemer@1.4.0: {}
graphql@16.9.0: {}
+ has-bigints@1.1.0: {}
+
has-flag@4.0.0: {}
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+
+ has-proto@1.2.0:
+ dependencies:
+ dunder-proto: 1.0.1
+
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
@@ -3819,22 +4982,140 @@ snapshots:
indent-string@4.0.0: {}
+ internal-slot@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ hasown: 2.0.2
+ side-channel: 1.1.0
+
+ is-array-buffer@3.0.5:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ is-async-function@2.1.1:
+ dependencies:
+ async-function: 1.0.0
+ call-bound: 1.0.4
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-bigint@1.1.0:
+ dependencies:
+ has-bigints: 1.1.0
+
+ is-boolean-object@1.2.2:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-bun-module@2.0.0:
+ dependencies:
+ semver: 7.7.2
+
+ is-callable@1.2.7: {}
+
+ is-core-module@2.16.1:
+ dependencies:
+ hasown: 2.0.2
+
+ is-data-view@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ is-typed-array: 1.1.15
+
+ is-date-object@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
is-extglob@2.1.1: {}
+ is-finalizationregistry@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
is-fullwidth-code-point@3.0.0: {}
+ is-generator-function@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
+ is-map@2.0.3: {}
+
+ is-negative-zero@2.0.3: {}
+
is-node-process@1.2.0: {}
+ is-number-object@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
is-number@7.0.0: {}
is-potential-custom-element-name@1.0.1: {}
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ is-set@2.0.3: {}
+
+ is-shared-array-buffer@1.0.4:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-string@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-symbol@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-symbols: 1.1.0
+ safe-regex-test: 1.1.0
+
+ is-typed-array@1.1.15:
+ dependencies:
+ which-typed-array: 1.1.19
+
+ is-weakmap@2.0.2: {}
+
+ is-weakref@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-weakset@2.0.4:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ isarray@2.0.5: {}
+
isexe@2.0.0: {}
+ jotai@2.13.1(@babel/core@7.28.0)(@babel/template@7.27.2)(@types/react@19.1.9)(react@19.1.1):
+ optionalDependencies:
+ '@babel/core': 7.28.0
+ '@babel/template': 7.27.2
+ '@types/react': 19.1.9
+ react: 19.1.1
+
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@@ -3878,8 +5159,18 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
+ json5@1.0.2:
+ dependencies:
+ minimist: 1.2.8
+
json5@2.2.3: {}
+ jsonfile@6.2.0:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -3889,6 +5180,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
+ locate-path@5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -3917,6 +5212,10 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
+ make-dir@3.1.0:
+ dependencies:
+ semver: 6.3.1
+
math-intrinsics@1.1.0: {}
merge2@1.4.1: {}
@@ -3942,6 +5241,8 @@ snapshots:
dependencies:
brace-expansion: 2.0.1
+ minimist@1.2.8: {}
+
mrmime@2.0.0: {}
ms@2.1.3: {}
@@ -3975,12 +5276,47 @@ snapshots:
nanoid@3.3.11: {}
+ napi-postinstall@0.3.3: {}
+
natural-compare@1.4.0: {}
node-releases@2.0.18: {}
nwsapi@2.2.16: {}
+ object-inspect@1.13.4: {}
+
+ object-keys@1.1.1: {}
+
+ object.assign@4.1.7:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+ has-symbols: 1.1.0
+ object-keys: 1.1.1
+
+ object.fromentries@2.0.8:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-object-atoms: 1.1.1
+
+ object.groupby@1.0.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+
+ object.values@1.2.1:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -3992,14 +5328,30 @@ snapshots:
outvariant@1.4.3: {}
+ own-keys@1.0.1:
+ dependencies:
+ get-intrinsic: 1.3.0
+ object-keys: 1.1.1
+ safe-push-apply: 1.0.0
+
+ p-limit@2.3.0:
+ dependencies:
+ p-try: 2.2.0
+
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
+ p-locate@4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
+ p-try@2.2.0: {}
+
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -4012,8 +5364,12 @@ snapshots:
path-key@3.1.1: {}
+ path-parse@1.0.7: {}
+
path-to-regexp@6.3.0: {}
+ path-type@4.0.0: {}
+
pathe@2.0.3: {}
pathval@2.0.0: {}
@@ -4026,6 +5382,12 @@ snapshots:
picomatch@4.0.3: {}
+ pkg-dir@4.2.0:
+ dependencies:
+ find-up: 4.1.0
+
+ possible-typed-array-names@1.1.0: {}
+
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@@ -4109,14 +5471,42 @@ snapshots:
indent-string: 4.0.0
strip-indent: 3.0.0
+ reflect.getprototypeof@1.0.10:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ which-builtin-type: 1.2.1
+
regenerator-runtime@0.14.1: {}
+ regexp.prototype.flags@1.5.4:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-errors: 1.3.0
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ set-function-name: 2.0.2
+
require-directory@2.1.1: {}
requires-port@1.0.0: {}
resolve-from@4.0.0: {}
+ resolve-pkg-maps@1.0.0: {}
+
+ resolve@1.22.10:
+ dependencies:
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
reusify@1.0.4: {}
rollup@4.46.2:
@@ -4151,6 +5541,25 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
+ safe-array-concat@1.1.3:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ has-symbols: 1.1.0
+ isarray: 2.0.5
+
+ safe-push-apply@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ isarray: 2.0.5
+
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
safer-buffer@2.1.2: {}
saxes@6.0.0:
@@ -4163,14 +5572,66 @@ snapshots:
semver@7.6.3: {}
+ semver@7.7.2: {}
+
set-cookie-parser@2.7.1: {}
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-function-name@2.0.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+
+ set-proto@1.0.0:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
siginfo@2.0.0: {}
signal-exit@4.1.0: {}
@@ -4181,14 +5642,23 @@ snapshots:
mrmime: 2.0.0
totalist: 3.0.1
+ slash@3.0.0: {}
+
source-map-js@1.2.1: {}
+ stable-hash-x@0.2.0: {}
+
stackback@0.0.2: {}
statuses@2.0.1: {}
std-env@3.9.0: {}
+ stop-iteration-iterator@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ internal-slot: 1.1.0
+
strict-event-emitter@0.5.1: {}
string-width@4.2.3:
@@ -4197,10 +5667,35 @@ snapshots:
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
+ string.prototype.trim@1.2.10:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-data-property: 1.1.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-object-atoms: 1.1.1
+ has-property-descriptors: 1.0.2
+
+ string.prototype.trimend@1.0.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ string.prototype.trimstart@1.0.8:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
+ strip-bom@3.0.0: {}
+
strip-indent@3.0.0:
dependencies:
min-indent: 1.0.1
@@ -4211,10 +5706,16 @@ snapshots:
dependencies:
js-tokens: 9.0.1
+ strip-outer@1.0.1:
+ dependencies:
+ escape-string-regexp: 1.0.5
+
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
+ supports-preserve-symlinks-flag@1.0.0: {}
+
symbol-tree@3.2.4: {}
tinybench@2.9.0: {}
@@ -4261,10 +5762,21 @@ snapshots:
dependencies:
punycode: 2.3.1
+ trim-repeated@1.0.0:
+ dependencies:
+ escape-string-regexp: 1.0.5
+
ts-api-utils@2.1.0(typescript@5.9.2):
dependencies:
typescript: 5.9.2
+ tsconfig-paths@3.15.0:
+ dependencies:
+ '@types/json5': 0.0.29
+ json5: 1.0.2
+ minimist: 1.2.8
+ strip-bom: 3.0.0
+
tslib@2.8.0: {}
type-check@0.4.0:
@@ -4275,6 +5787,39 @@ snapshots:
type-fest@4.26.1: {}
+ typed-array-buffer@1.0.3:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-length@1.0.3:
+ dependencies:
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-offset@1.0.4:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+ reflect.getprototypeof: 1.0.10
+
+ typed-array-length@1.0.7:
+ dependencies:
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ is-typed-array: 1.1.15
+ possible-typed-array-names: 1.1.0
+ reflect.getprototypeof: 1.0.10
+
typescript-eslint@8.39.0(eslint@9.33.0)(typescript@5.9.2):
dependencies:
'@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/parser@8.39.0(eslint@9.33.0)(typescript@5.9.2))(eslint@9.33.0)(typescript@5.9.2)
@@ -4288,10 +5833,43 @@ snapshots:
typescript@5.9.2: {}
+ unbox-primitive@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-bigints: 1.1.0
+ has-symbols: 1.1.0
+ which-boxed-primitive: 1.1.1
+
undici-types@6.19.8: {}
universalify@0.2.0: {}
+ universalify@2.0.1: {}
+
+ unrs-resolver@1.11.1:
+ dependencies:
+ napi-postinstall: 0.3.3
+ optionalDependencies:
+ '@unrs/resolver-binding-android-arm-eabi': 1.11.1
+ '@unrs/resolver-binding-android-arm64': 1.11.1
+ '@unrs/resolver-binding-darwin-arm64': 1.11.1
+ '@unrs/resolver-binding-darwin-x64': 1.11.1
+ '@unrs/resolver-binding-freebsd-x64': 1.11.1
+ '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1
+ '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1
+ '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-arm64-musl': 1.11.1
+ '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1
+ '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-x64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-x64-musl': 1.11.1
+ '@unrs/resolver-binding-wasm32-wasi': 1.11.1
+ '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1
+ '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
+ '@unrs/resolver-binding-win32-x64-msvc': 1.11.1
+
update-browserslist-db@1.1.1(browserslist@4.24.2):
dependencies:
browserslist: 4.24.2
@@ -4425,6 +6003,47 @@ snapshots:
tr46: 5.1.1
webidl-conversions: 7.0.0
+ which-boxed-primitive@1.1.1:
+ dependencies:
+ is-bigint: 1.1.0
+ is-boolean-object: 1.2.2
+ is-number-object: 1.1.1
+ is-string: 1.1.1
+ is-symbol: 1.1.1
+
+ which-builtin-type@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ function.prototype.name: 1.1.8
+ has-tostringtag: 1.0.2
+ is-async-function: 2.1.1
+ is-date-object: 1.1.0
+ is-finalizationregistry: 1.1.1
+ is-generator-function: 1.1.0
+ is-regex: 1.2.1
+ is-weakref: 1.1.1
+ isarray: 2.0.5
+ which-boxed-primitive: 1.1.1
+ which-collection: 1.0.2
+ which-typed-array: 1.1.19
+
+ which-collection@1.0.2:
+ dependencies:
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-weakmap: 2.0.2
+ is-weakset: 2.0.4
+
+ which-typed-array@1.1.19:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
which@2.0.2:
dependencies:
isexe: 2.0.0
diff --git a/src/App.tsx b/src/App.tsx
index 0c0032aab..edf29c34c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,20 +1,17 @@
-import { BrowserRouter as Router } from "react-router-dom"
-import Header from "./components/Header.tsx"
-import Footer from "./components/Footer.tsx"
-import PostsManagerPage from "./pages/PostsManagerPage.tsx"
+import { BrowserRouter as Router } from "react-router-dom";
+
+import { RootLayout } from "@/app/ui/layout.tsx";
+
+import PostsManagerPage from "./pages/posts-manager-page.tsx";
const App = () => {
return (
-
+
+
+
- )
-}
+ );
+};
-export default App
+export default App;
diff --git a/src/app/ui/layout.tsx b/src/app/ui/layout.tsx
new file mode 100644
index 000000000..5639d5a2a
--- /dev/null
+++ b/src/app/ui/layout.tsx
@@ -0,0 +1,14 @@
+import { Footer } from "@/widgets/footer/ui/footer";
+import { Header } from "@/widgets/header/ui/header";
+
+import type { PropsWithChildren } from "react";
+
+export function RootLayout({ children }: PropsWithChildren) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/ui/query-provider.tsx b/src/app/ui/query-provider.tsx
new file mode 100644
index 000000000..984cfcdc4
--- /dev/null
+++ b/src/app/ui/query-provider.tsx
@@ -0,0 +1,13 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import { PropsWithChildren, useState } from "react";
+
+export function QueryProvider({ children }: PropsWithChildren) {
+ const [client] = useState(() => new QueryClient());
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
deleted file mode 100644
index 91af02f8c..000000000
--- a/src/components/Footer.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-
-const Footer: React.FC = () => {
- return (
-
- );
-};
-
-export default Footer;
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
deleted file mode 100644
index 63ecec168..000000000
--- a/src/components/Header.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-import { MessageSquare } from 'lucide-react';
-
-const Header: React.FC = () => {
- return (
-
-
-
-
-
게시물 관리 시스템
-
-
-
-
- );
-};
-
-export default Header;
-
diff --git a/src/components/index.tsx b/src/components/index.tsx
deleted file mode 100644
index 8495817d3..000000000
--- a/src/components/index.tsx
+++ /dev/null
@@ -1,214 +0,0 @@
-import * as React from "react"
-import { forwardRef } from "react"
-import * as SelectPrimitive from "@radix-ui/react-select"
-import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { Check, ChevronDown, X } from "lucide-react"
-import { cva, VariantProps } from "class-variance-authority"
-
-const buttonVariants = cva(
- "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
- {
- variants: {
- variant: {
- default: "bg-blue-500 text-white hover:bg-blue-600",
- destructive: "bg-red-500 text-white hover:bg-red-600",
- outline: "border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-100",
- secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
- ghost: "bg-transparent text-gray-700 hover:bg-gray-100",
- link: "underline-offset-4 hover:underline text-blue-500",
- },
- size: {
- default: "h-10 py-2 px-4",
- sm: "h-8 px-3 rounded-md text-xs",
- lg: "h-11 px-8 rounded-md",
- icon: "h-9 w-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- },
-)
-
-interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps {
- className?: string
-}
-
-export const Button = forwardRef(({ className, variant, size, ...props }, ref) => {
- return
-})
-
-Button.displayName = "Button"
-
-// 입력 컴포넌트
-export const Input = forwardRef(({ className, type, ...props }, ref) => {
- return (
-
- )
-})
-Input.displayName = "Input"
-
-// 카드 컴포넌트
-export const Card = forwardRef(({ className, ...props }, ref) => (
-
-))
-Card.displayName = "Card"
-
-export const CardHeader = forwardRef(({ className, ...props }, ref) => (
-
-))
-CardHeader.displayName = "CardHeader"
-
-export const CardTitle = forwardRef(({ className, ...props }, ref) => (
-
-))
-CardTitle.displayName = "CardTitle"
-
-export const CardContent = forwardRef(({ className, ...props }, ref) => (
-
-))
-CardContent.displayName = "CardContent"
-
-// 텍스트 영역 컴포넌트
-export const Textarea = forwardRef(({ className, ...props }, ref) => {
- return (
-
- )
-})
-Textarea.displayName = "Textarea"
-
-// 선택 컴포넌트
-export const Select = SelectPrimitive.Root
-export const SelectGroup = SelectPrimitive.Group
-export const SelectValue = SelectPrimitive.Value
-
-export const SelectTrigger = forwardRef(({ className, children, ...props }, ref) => (
-
- {children}
-
-
-))
-SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
-
-export const SelectContent = forwardRef(({ className, children, position = "popper", ...props }, ref) => (
-
-
- {children}
-
-
-))
-SelectContent.displayName = SelectPrimitive.Content.displayName
-
-export const SelectItem = forwardRef(({ className, children, ...props }, ref) => (
-
-
-
-
-
-
- {children}
-
-))
-SelectItem.displayName = SelectPrimitive.Item.displayName
-
-// 대화상자 컴포넌트
-export const Dialog = DialogPrimitive.Root
-export const DialogTrigger = DialogPrimitive.Trigger
-export const DialogPortal = DialogPrimitive.Portal
-export const DialogOverlay = DialogPrimitive.Overlay
-
-export const DialogContent = forwardRef(({ className, children, ...props }, ref) => (
-
-
-
- {children}
-
-
- 닫기
-
-
-
-))
-DialogContent.displayName = DialogPrimitive.Content.displayName
-
-export const DialogHeader = ({ className, ...props }) => (
-
-)
-DialogHeader.displayName = "DialogHeader"
-
-export const DialogTitle = forwardRef(({ className, ...props }, ref) => (
-
-))
-DialogTitle.displayName = DialogPrimitive.Title.displayName
-
-// 테이블 컴포넌트
-export const Table = forwardRef(({ className, ...props }, ref) => (
-
-))
-Table.displayName = "Table"
-
-export const TableHeader = forwardRef(({ className, ...props }, ref) => (
-
-))
-TableHeader.displayName = "TableHeader"
-
-export const TableBody = forwardRef(({ className, ...props }, ref) => (
-
-))
-TableBody.displayName = "TableBody"
-
-export const TableRow = forwardRef(({ className, ...props }, ref) => (
-
-))
-TableRow.displayName = "TableRow"
-
-export const TableHead = forwardRef(({ className, ...props }, ref) => (
- |
-))
-TableHead.displayName = "TableHead"
-
-export const TableCell = forwardRef(({ className, ...props }, ref) => (
- |
-))
-TableCell.displayName = "TableCell"
diff --git a/src/entities/comment/api/comments.api.ts b/src/entities/comment/api/comments.api.ts
new file mode 100644
index 000000000..b37ce6eef
--- /dev/null
+++ b/src/entities/comment/api/comments.api.ts
@@ -0,0 +1,40 @@
+import type { Comment } from "@/entities/comment/model/comment.types";
+import { http } from "@/shared/api/client";
+
+export const commentApi = {
+ get(postId: number): Promise {
+ return http.get(`/comments/post/${postId}`);
+ },
+ create({ body, postId, userId }: CreateCommentPayload): Promise {
+ return http.post("/comments/add", { body, postId, userId });
+ },
+ update(payload: UpdateCommentPayload): Promise {
+ const { id, body } = payload;
+ return http.put(`/comments/${id}`, { body });
+ },
+ remove(id: number): Promise {
+ return http.delete(`/comments/${id}`);
+ },
+ like({ id, likes }: LikeCommentPayload): Promise {
+ return http.patch(`/comments/${id}`, { likes });
+ },
+} as const;
+
+export interface GetCommentsByPostIdResponse {
+ comments: Comment[];
+}
+export interface CreateCommentPayload {
+ body: string;
+ postId: number;
+ userId: number;
+}
+
+export interface UpdateCommentPayload {
+ id: number;
+ body: string;
+}
+
+export interface LikeCommentPayload {
+ id: number;
+ likes: number;
+}
diff --git a/src/entities/comment/index.ts b/src/entities/comment/index.ts
new file mode 100644
index 000000000..467e88117
--- /dev/null
+++ b/src/entities/comment/index.ts
@@ -0,0 +1,20 @@
+export { commentApi } from "./api/comments.api";
+export type {
+ GetCommentsByPostIdResponse,
+ CreateCommentPayload,
+ UpdateCommentPayload,
+ LikeCommentPayload,
+} from "./api/comments.api";
+
+export {
+ useCommentsQuery,
+ useCreateCommentMutation,
+ useUpdateCommentMutation,
+ useDeleteCommentMutation,
+ useLikeCommentMutation,
+} from "./model/comment.query";
+export { useComments } from "./model/comment.hook";
+
+export type { CommentUser, Comment, CommentsByPostId } from "./model/comment.types";
+export { CommentList } from "./ui/comment-list";
+export { CommentAddDialog, CommentEditDialog } from "./ui";
diff --git a/src/entities/comment/model/comment.atom.ts b/src/entities/comment/model/comment.atom.ts
new file mode 100644
index 000000000..ec31d70e0
--- /dev/null
+++ b/src/entities/comment/model/comment.atom.ts
@@ -0,0 +1,5 @@
+import { atom } from "jotai";
+
+import type { Comment } from "./comment.types";
+
+export const selectedCommentAtom = atom(null);
diff --git a/src/entities/comment/model/comment.hook.ts b/src/entities/comment/model/comment.hook.ts
new file mode 100644
index 000000000..2b421fe11
--- /dev/null
+++ b/src/entities/comment/model/comment.hook.ts
@@ -0,0 +1,12 @@
+import { useAtom } from "jotai";
+
+import { selectedCommentAtom } from "./comment.atom";
+
+export const useComments = () => {
+ const [selectedComment, setSelectedComment] = useAtom(selectedCommentAtom);
+
+ return {
+ selectedComment,
+ setSelectedComment,
+ };
+};
diff --git a/src/entities/comment/model/comment.keys.ts b/src/entities/comment/model/comment.keys.ts
new file mode 100644
index 000000000..ac53c63e2
--- /dev/null
+++ b/src/entities/comment/model/comment.keys.ts
@@ -0,0 +1,4 @@
+export const commentQueryKeys = {
+ all: ["comments"] as const,
+ byPost: (postId: number) => ["comments", postId] as const,
+} as const;
diff --git a/src/entities/comment/model/comment.query.ts b/src/entities/comment/model/comment.query.ts
new file mode 100644
index 000000000..aed2f74eb
--- /dev/null
+++ b/src/entities/comment/model/comment.query.ts
@@ -0,0 +1,136 @@
+import { useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
+
+import { commentApi } from "@/entities/comment";
+
+import { commentQueryKeys } from "./comment.keys";
+
+import type { Comment } from "./comment.types";
+
+export function useCommentsQuery(postId?: number) {
+ return useQuery({
+ enabled: !!postId,
+ queryKey: postId ? commentQueryKeys.byPost(postId) : commentQueryKeys.all,
+ queryFn: () => commentApi.get(postId as number).then((res) => res.comments),
+ staleTime: 30_000,
+ });
+}
+
+export const commentsByPostQuery = (postId: number) => ({
+ queryKey: commentQueryKeys.byPost(postId),
+ queryFn: () => commentApi.get(postId).then((res) => res.comments),
+});
+
+export const getCommentsFromCache = (client: QueryClient, postId: number): Comment[] => {
+ return client.getQueryData(commentQueryKeys.byPost(postId)) ?? [];
+};
+
+export const prefetchCommentsByPost = (client: QueryClient, postId: number) =>
+ client.prefetchQuery(commentsByPostQuery(postId));
+
+export const ensureCommentsByPost = (client: QueryClient, postId: number) =>
+ client.ensureQueryData(commentsByPostQuery(postId));
+
+export function useCreateCommentMutation() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: commentApi.create,
+ onMutate: async (payload) => {
+ const key = commentQueryKeys.byPost(payload.postId);
+ const prev = queryClient.getQueryData(key);
+ const optimistic = {
+ id: Date.now(),
+ body: payload.body,
+ postId: payload.postId,
+ likes: 0,
+ user: { id: payload.userId, username: "you" },
+ } satisfies Comment;
+ queryClient.setQueryData(key, [optimistic, ...(prev ?? [])]);
+ return { key, prev, optimisticId: optimistic.id } as const;
+ },
+ onError: (_error, _variables, context) => {
+ if (!context) return;
+ queryClient.setQueryData(context.key, context.prev);
+ },
+ onSuccess: (created, _variables, context) => {
+ if (!context) return;
+ const list = (queryClient.getQueryData(context.key) ?? []).map((c) =>
+ c.id === context.optimisticId ? created : c,
+ );
+ queryClient.setQueryData(context.key, list);
+ },
+ });
+}
+
+export interface UpdateCommentVariables {
+ postId: number;
+ id: number;
+ body: string;
+}
+
+export function useUpdateCommentMutation() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (variables: UpdateCommentVariables) => commentApi.update({ id: variables.id, body: variables.body }),
+ onMutate: async (variables) => {
+ const key = commentQueryKeys.byPost(variables.postId);
+ const prev = queryClient.getQueryData(key);
+ queryClient.setQueryData(
+ key,
+ (prev ?? []).map((c) => (c.id === variables.id ? { ...c, body: variables.body } : c)),
+ );
+ return { key, prev } as const;
+ },
+ onError: (_error, _variables, context) => context && queryClient.setQueryData(context.key, context.prev),
+ onSuccess: (updated, _variables, context) => {
+ if (!context) return;
+ queryClient.setQueryData(context.key, (prev) =>
+ (prev ?? []).map((comment) => (comment.id === updated.id ? updated : comment)),
+ );
+ },
+ });
+}
+
+export function useDeleteCommentMutation() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id }: { id: number; postId: number }) => commentApi.remove(id),
+ onMutate: async ({ id, postId }: { id: number; postId: number }) => {
+ const key = commentQueryKeys.byPost(postId);
+ const prev = queryClient.getQueryData(key);
+ queryClient.setQueryData(
+ key,
+ (prev ?? []).filter((c) => c.id !== id),
+ );
+ return { key, prev } as const;
+ },
+ onError: (_error, _variables, context) => context && queryClient.setQueryData(context.key, context.prev),
+ });
+}
+
+export function useLikeCommentMutation() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: commentApi.like,
+ onMutate: async ({ id, postId, likes }: { id: number; postId: number; likes: number }) => {
+ const key = commentQueryKeys.byPost(postId);
+ const prev = queryClient.getQueryData(key);
+ const previousLikes = (prev ?? []).find((c) => c.id === id)?.likes ?? 0;
+ queryClient.setQueryData(
+ key,
+ (prev ?? []).map((c) => (c.id === id ? { ...c, likes } : c)),
+ );
+ return { key, prev, id, optimisticLikes: likes, previousLikes } as const;
+ },
+ onError: (_error, _variables, context) => context && queryClient.setQueryData(context.key, context.prev),
+ onSuccess: (updated, _variables, context) => {
+ if (!context) return;
+ queryClient.setQueryData(context.key, (prev) =>
+ (prev ?? []).map((c) =>
+ c.id === updated.id
+ ? { ...c, likes: Math.max(context.optimisticLikes, updated.likes ?? context.previousLikes) }
+ : c,
+ ),
+ );
+ },
+ });
+}
diff --git a/src/entities/comment/model/comment.types.ts b/src/entities/comment/model/comment.types.ts
new file mode 100644
index 000000000..8e948722d
--- /dev/null
+++ b/src/entities/comment/model/comment.types.ts
@@ -0,0 +1,16 @@
+export interface CommentUser {
+ id: number;
+ username: string;
+ fullName?: string;
+ image?: string;
+}
+
+export interface Comment {
+ id: number;
+ postId: number;
+ user: CommentUser;
+ body: string;
+ likes: number;
+}
+
+export type CommentsByPostId = Record;
diff --git a/src/entities/comment/ui/comment-add-dialog.tsx b/src/entities/comment/ui/comment-add-dialog.tsx
new file mode 100644
index 000000000..aaa76d6b8
--- /dev/null
+++ b/src/entities/comment/ui/comment-add-dialog.tsx
@@ -0,0 +1,29 @@
+import { ChangeEventHandler } from "react";
+
+import { Button } from "@shared/ui/button";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@shared/ui/dialog";
+import { Textarea } from "@shared/ui/textarea";
+
+export interface CommentAddDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ body: string;
+ onChange: ChangeEventHandler | undefined;
+ onSubmit: () => Promise | void;
+}
+
+export function CommentAddDialog({ open, onOpenChange, body, onChange, onSubmit }: Readonly) {
+ return (
+
+ );
+}
diff --git a/src/entities/comment/ui/comment-edit-dialog.tsx b/src/entities/comment/ui/comment-edit-dialog.tsx
new file mode 100644
index 000000000..5f4464e1a
--- /dev/null
+++ b/src/entities/comment/ui/comment-edit-dialog.tsx
@@ -0,0 +1,37 @@
+import { ChangeEventHandler } from "react";
+
+import { Button } from "@shared/ui/button";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@shared/ui/dialog";
+import { Textarea } from "@shared/ui/textarea";
+
+import type { Comment } from "../model/comment.types";
+
+export interface CommentEditDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ comment: Comment | null;
+ onChange: ChangeEventHandler | undefined;
+ onSubmit: () => Promise | void;
+}
+
+export function CommentEditDialog({
+ open,
+ onOpenChange,
+ comment,
+ onChange,
+ onSubmit,
+}: Readonly) {
+ return (
+
+ );
+}
diff --git a/src/entities/comment/ui/comment-list.tsx b/src/entities/comment/ui/comment-list.tsx
new file mode 100644
index 000000000..b136ca676
--- /dev/null
+++ b/src/entities/comment/ui/comment-list.tsx
@@ -0,0 +1,45 @@
+import { ThumbsUp, Edit2, Trash2 } from "lucide-react";
+
+import type { Comment } from "@/entities/comment/model/comment.types";
+import { splitByHighlight } from "@/shared/lib/split-by-highlight";
+import { Button } from "@/shared/ui/button";
+import { HighlightText } from "@/shared/ui/highlight-text";
+
+export interface CommentListProps {
+ comments: Comment[] | undefined;
+ searchQuery: string;
+ onLike: (id: number) => void;
+ onEdit: (comment: Comment) => void;
+ onDelete: (id: number) => void;
+}
+
+export function CommentList({ comments, searchQuery, onLike, onEdit, onDelete }: CommentListProps) {
+ if (!comments || comments.length === 0) return null;
+
+ return (
+
+ {comments.map((comment) => (
+
+
+ {comment.user.username}:
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/entities/comment/ui/index.ts b/src/entities/comment/ui/index.ts
new file mode 100644
index 000000000..506d7ee96
--- /dev/null
+++ b/src/entities/comment/ui/index.ts
@@ -0,0 +1,2 @@
+export { CommentAddDialog } from "./comment-add-dialog";
+export { CommentEditDialog } from "./comment-edit-dialog";
diff --git a/src/entities/post/api/posts.api.ts b/src/entities/post/api/posts.api.ts
new file mode 100644
index 000000000..4ce649726
--- /dev/null
+++ b/src/entities/post/api/posts.api.ts
@@ -0,0 +1,61 @@
+import { http } from "@/shared/api/client";
+
+import { Post } from "../model/post.types";
+
+import type { Tag } from "../model/post.types";
+
+export const postApi = {
+ get({ limit, skip, sortBy, order }: PostsParams): Promise {
+ return http.get("/posts", { params: { limit, skip, sortBy, order } });
+ },
+ create(payload: CreatePostParams): Promise {
+ return http.post("/posts/add", payload);
+ },
+ update({ postId, params }: UpdatePostPayload): Promise {
+ return http.put(`/posts/${postId}`, params);
+ },
+ remove(id: number): Promise {
+ return http.delete(`/posts/${id}`);
+ },
+ async getByTag(
+ tag: string,
+ params?: Pick,
+ ): Promise {
+ return http.get(`/posts/tag/${tag}`, { params });
+ },
+ async search(
+ query: string,
+ params?: Pick,
+ ): Promise {
+ return http.get(`/posts/search`, { params: { q: query, ...params } });
+ },
+ async getTags(): Promise {
+ return http.get(`/posts/tags`);
+ },
+} as const;
+
+export type CreatePostParams = {
+ title: string;
+ body?: string;
+ userId: number;
+};
+
+export type UpdatePostPayload = {
+ postId: string;
+ params: {
+ title: string;
+ body: string;
+ };
+};
+
+export interface PostsParams {
+ limit: number;
+ skip: number;
+ sortBy?: string;
+ order?: "asc" | "desc";
+}
+
+export interface PostsResponse {
+ posts: Post[];
+ total: number;
+}
diff --git a/src/entities/post/index.ts b/src/entities/post/index.ts
new file mode 100644
index 000000000..09f3f2a61
--- /dev/null
+++ b/src/entities/post/index.ts
@@ -0,0 +1,17 @@
+export type { Post, Tag } from "./model/post.types";
+
+export type { CreatePostParams, UpdatePostPayload } from "./api/posts.api";
+export type { PostsParams, PostsResponse } from "./api/posts.api";
+
+export { usePosts } from "./model/post.hook";
+
+export { postApi } from "./api/posts.api";
+
+export { PostAddDialog } from "./ui/post-add-dialog";
+
+export { PostEditDialog } from "./ui/post-edit-dialog";
+
+export { PostDetailDialog } from "./ui/post-detail-dialog";
+
+export { PostsTable } from "./ui/posts-table";
+export type { PostsTableProps } from "./ui/posts-table";
diff --git a/src/entities/post/model/post.atom.ts b/src/entities/post/model/post.atom.ts
new file mode 100644
index 000000000..e1459bc36
--- /dev/null
+++ b/src/entities/post/model/post.atom.ts
@@ -0,0 +1,6 @@
+import { atom } from "jotai";
+
+import { Post } from "./post.types";
+
+export const selectedPostAtom = atom(null);
+export const isPostDetailDialogOpenAtom = atom(false);
diff --git a/src/entities/post/model/post.hook.ts b/src/entities/post/model/post.hook.ts
new file mode 100644
index 000000000..c8286bb7b
--- /dev/null
+++ b/src/entities/post/model/post.hook.ts
@@ -0,0 +1,15 @@
+import { useAtom } from "jotai";
+
+import { isPostDetailDialogOpenAtom, selectedPostAtom } from "./post.atom";
+
+export const usePosts = () => {
+ const [selectedPost, setSelectedPost] = useAtom(selectedPostAtom);
+ const [isDetailOpen, setIsDetailOpen] = useAtom(isPostDetailDialogOpenAtom);
+
+ return {
+ selectedPost,
+ isDetailOpen,
+ setSelectedPost,
+ setIsDetailOpen,
+ };
+};
diff --git a/src/entities/post/model/post.keys.ts b/src/entities/post/model/post.keys.ts
new file mode 100644
index 000000000..72796c05f
--- /dev/null
+++ b/src/entities/post/model/post.keys.ts
@@ -0,0 +1,13 @@
+export const postsQueryKeys = {
+ all: ["posts"] as const,
+ list: (params: { limit: number; skip: number; sortBy?: string; order?: "asc" | "desc" }) =>
+ ["posts", "list", params] as const,
+ byTag: (tag: string, params?: { limit?: number; skip?: number; sortBy?: string; order?: "asc" | "desc" }) =>
+ ["posts", "tag", tag, params] as const,
+ search: (query: string, params?: { limit?: number; skip?: number; sortBy?: string; order?: "asc" | "desc" }) =>
+ ["posts", "search", { q: query, ...params }] as const,
+ detail: (id: number) => ["posts", "detail", id] as const,
+ tags: ["posts", "tags"] as const,
+} as const;
+
+export type PostsListParams = { limit: number; skip: number; sortBy?: string; order?: "asc" | "desc" };
diff --git a/src/entities/post/model/post.query.ts b/src/entities/post/model/post.query.ts
new file mode 100644
index 000000000..ee41a7074
--- /dev/null
+++ b/src/entities/post/model/post.query.ts
@@ -0,0 +1,128 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { postsQueryKeys } from "./post.keys";
+import { CreatePostParams, postApi, UpdatePostPayload } from "../api/posts.api";
+
+import type { Post } from "./post.types";
+
+export const postsSearchQuery = (query: string) => ({
+ queryKey: postsQueryKeys.search(query),
+ queryFn: () => postApi.search(query),
+});
+
+export function useCreatePostMutation() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (payload: CreatePostParams) => postApi.create(payload),
+ onMutate: async (payload) => {
+ const listQueries = queryClient.getQueryCache().findAll({ queryKey: postsQueryKeys.all });
+ const previousSnapshots = listQueries.map((q) => ({
+ key: q.queryKey,
+ data: q.state.data as { posts: Post[]; total: number } | undefined,
+ }));
+
+ const currentMaxId = listQueries.reduce((maxId, query) => {
+ const snapshot = (query.state.data as { posts: Post[]; total: number } | undefined)?.posts ?? [];
+ const localMax = snapshot.reduce((m, p) => (p.id > m ? p.id : m), 0);
+ return localMax > maxId ? localMax : maxId;
+ }, 0);
+ const optimisticId = currentMaxId + 1;
+ const optimisticPost: Post = {
+ id: optimisticId,
+ title: payload.title,
+ body: payload.body,
+ userId: payload.userId,
+ };
+
+ listQueries.forEach((q) => {
+ const data = (q.state.data as { posts: Post[]; total: number } | undefined) ?? { posts: [], total: 0 };
+ queryClient.setQueryData(q.queryKey, { posts: [optimisticPost, ...data.posts], total: (data.total ?? 0) + 1 });
+ });
+
+ return { previousSnapshots, optimisticId } as const;
+ },
+ onError: (_error, _variables, context) => {
+ if (!context) return;
+ context.previousSnapshots.forEach((s) => queryClient.setQueryData(s.key, s.data));
+ },
+ onSuccess: async (created, _variables, context) => {
+ const listQueries = queryClient.getQueryCache().findAll({ queryKey: postsQueryKeys.all });
+ listQueries.forEach((q) => {
+ const data = (q.state.data as { posts: Post[]; total: number } | undefined) ?? { posts: [], total: 0 };
+ const replaced = context
+ ? data.posts.map((p: Post) => (p.id === context.optimisticId ? created : p))
+ : [created, ...data.posts];
+ queryClient.setQueryData(q.queryKey, { posts: replaced, total: data.total });
+ });
+ await queryClient.invalidateQueries({ queryKey: postsQueryKeys.all });
+ },
+ });
+}
+
+export function useUpdatePostMutation() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (payload: UpdatePostPayload) => postApi.update(payload),
+ onMutate: async (payload) => {
+ const listQueries = queryClient.getQueryCache().findAll({ queryKey: postsQueryKeys.all });
+ const previousSnapshots = listQueries.map((q) => ({
+ key: q.queryKey,
+ data: q.state.data as { posts: Post[]; total: number } | undefined,
+ }));
+
+ const id = Number(payload.postId);
+ listQueries.forEach((q) => {
+ const data = (q.state.data as { posts: Post[]; total: number } | undefined) ?? { posts: [], total: 0 };
+ const replaced = data.posts.map((p: Post) => (p.id === id ? { ...p, ...payload.params } : p));
+ queryClient.setQueryData(q.queryKey, { posts: replaced, total: data.total });
+ });
+
+ return { previousSnapshots } as const;
+ },
+ onError: (_err, _variables, context) => {
+ if (!context) return;
+ context.previousSnapshots.forEach((s) => queryClient.setQueryData(s.key, s.data));
+ },
+ onSuccess: async (updated) => {
+ const listQueries = queryClient.getQueryCache().findAll({ queryKey: postsQueryKeys.all });
+ listQueries.forEach((q) => {
+ const data = (q.state.data as { posts: Post[]; total: number } | undefined) ?? { posts: [], total: 0 };
+ const replaced = data.posts.map((p: Post) => (p.id === updated.id ? { ...p, ...updated } : p));
+ queryClient.setQueryData(q.queryKey, { posts: replaced, total: data.total });
+ });
+ await queryClient.invalidateQueries({ queryKey: postsQueryKeys.all });
+ },
+ });
+}
+
+export function useDeletePostMutation() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (id: number) => postApi.remove(id),
+ onMutate: async (id) => {
+ const listQueries = queryClient.getQueryCache().findAll({ queryKey: postsQueryKeys.all });
+ const previousSnapshots = listQueries.map((q) => ({
+ key: q.queryKey,
+ data: q.state.data as { posts: Post[]; total: number } | undefined,
+ }));
+
+ listQueries.forEach((q) => {
+ const data = (q.state.data as { posts: Post[]; total: number } | undefined) ?? { posts: [], total: 0 };
+ const filtered = data.posts.filter((p: Post) => p.id !== id);
+ queryClient.setQueryData(q.queryKey, { posts: filtered, total: Math.max(0, (data.total ?? 0) - 1) });
+ });
+
+ return { previousSnapshots } as const;
+ },
+ onError: (_err, _variables, context) => {
+ if (!context) return;
+ context.previousSnapshots.forEach((s) => queryClient.setQueryData(s.key, s.data));
+ },
+ onSuccess: async () => {
+ await queryClient.invalidateQueries({ queryKey: postsQueryKeys.all });
+ },
+ });
+}
diff --git a/src/entities/post/model/post.types.ts b/src/entities/post/model/post.types.ts
new file mode 100644
index 000000000..66d9b7726
--- /dev/null
+++ b/src/entities/post/model/post.types.ts
@@ -0,0 +1,13 @@
+import { User } from "@/entities/user";
+
+export type Post = {
+ id: number;
+ title: string;
+ body?: string;
+ userId?: number;
+ tags?: string[];
+ reactions?: { likes: number; dislikes: number };
+ author?: User;
+};
+
+export type Tag = { slug: string; url?: string };
diff --git a/src/entities/post/ui/post-add-dialog.tsx b/src/entities/post/ui/post-add-dialog.tsx
new file mode 100644
index 000000000..8e4a13012
--- /dev/null
+++ b/src/entities/post/ui/post-add-dialog.tsx
@@ -0,0 +1,44 @@
+import { Button } from "@shared/ui/button";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@shared/ui/dialog";
+import { Input } from "@shared/ui/input";
+import { Textarea } from "@shared/ui/textarea";
+
+export interface PostAddDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ value: { title: string; body: string; userId: number };
+ onChange: (next: { title: string; body: string; userId: number }) => void;
+ onSubmit: () => Promise | void;
+}
+
+export function PostAddDialog({ open, onOpenChange, value, onChange, onSubmit }: Readonly) {
+ return (
+
+ );
+}
diff --git a/src/entities/post/ui/post-detail-dialog.tsx b/src/entities/post/ui/post-detail-dialog.tsx
new file mode 100644
index 000000000..151857ad5
--- /dev/null
+++ b/src/entities/post/ui/post-detail-dialog.tsx
@@ -0,0 +1,34 @@
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@shared/ui/dialog";
+import { HighlightText } from "@shared/ui/highlight-text";
+
+import { CommentsListContainer } from "@/features/comment-edit";
+import { splitByHighlight } from "@/shared/lib/split-by-highlight";
+
+import type { Post } from "../model/post.types";
+
+export interface PostDetailDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ post: Post | null;
+ searchQuery: string;
+}
+
+export function PostDetailDialog({ open, onOpenChange, post, searchQuery }: Readonly) {
+ return (
+
+ );
+}
diff --git a/src/entities/post/ui/post-edit-dialog.tsx b/src/entities/post/ui/post-edit-dialog.tsx
new file mode 100644
index 000000000..b21357950
--- /dev/null
+++ b/src/entities/post/ui/post-edit-dialog.tsx
@@ -0,0 +1,40 @@
+import { Button } from "@shared/ui/button";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@shared/ui/dialog";
+import { Input } from "@shared/ui/input";
+import { Textarea } from "@shared/ui/textarea";
+
+import type { Post } from "../model/post.types";
+
+export interface PostEditDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ post: Post | null;
+ onChange: (next: Post) => void;
+ onSubmit: () => Promise | void;
+}
+
+export function PostEditDialog({ open, onOpenChange, post, onChange, onSubmit }: Readonly) {
+ return (
+
+ );
+}
diff --git a/src/entities/post/ui/post-filter.tsx b/src/entities/post/ui/post-filter.tsx
new file mode 100644
index 000000000..68b8f507a
--- /dev/null
+++ b/src/entities/post/ui/post-filter.tsx
@@ -0,0 +1,83 @@
+import { Search } from "lucide-react";
+import { ChangeEventHandler } from "react";
+
+import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/ui";
+
+import { Tag } from "../model/post.types";
+
+interface PostFilterProps {
+ searchQuery: string;
+ onChange: ChangeEventHandler;
+ onEnter?: () => void;
+ tags: Tag[];
+ selectedTag: string;
+ onChangeTag: (value: string) => void;
+ sortBy: string;
+ onChangeSortBy: (value: string) => void;
+ sortOrder: string;
+ onChangeSortOrder: (value: string) => void;
+}
+
+export function PostFilter({
+ searchQuery,
+ onChange,
+ onEnter,
+ tags,
+ selectedTag,
+ onChangeTag,
+ sortBy,
+ onChangeSortBy,
+ sortOrder,
+ onChangeSortOrder,
+}: Readonly) {
+ return (
+
+
+
+
+ {
+ if (e.key === "Enter") onEnter?.();
+ }}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/src/entities/post/ui/posts-table.tsx b/src/entities/post/ui/posts-table.tsx
new file mode 100644
index 000000000..753a8cb0a
--- /dev/null
+++ b/src/entities/post/ui/posts-table.tsx
@@ -0,0 +1,136 @@
+import { Edit2, MessageSquare, ThumbsDown, ThumbsUp, Trash2 } from "lucide-react";
+
+import type { HighlightSegment } from "@shared/lib/split-by-highlight";
+import { Table, Button, TableCell, TableRow, TableBody, TableHead, TableHeader } from "@shared/ui";
+import {} from "@shared/ui";
+import { HighlightText } from "@shared/ui/highlight-text";
+
+import type { Post } from "../model/post.types";
+
+type PostRowProps = {
+ post: Post;
+ titleSegments: HighlightSegment[];
+ selectedTag?: string;
+ onClickTag: (tag: string) => void;
+ onOpenUser: (user: Post["author"]) => void;
+ onOpenDetail: (post: Post) => void;
+ onEdit: (post: Post) => void;
+ onDelete: (id: number) => void;
+};
+
+export function PostRow({
+ post,
+ titleSegments,
+ selectedTag,
+ onClickTag,
+ onOpenUser,
+ onOpenDetail,
+ onEdit,
+ onDelete,
+}: PostRowProps) {
+ return (
+
+ {post.id}
+
+
+
+
+
+
+ {post.tags?.map((tag) => (
+ onClickTag(tag)}
+ >
+ {tag}
+
+ ))}
+
+
+
+
+ onOpenUser(post.author)}>
+

+
{post.author?.username}
+
+
+
+
+
+ {post.reactions?.likes || 0}
+
+ {post.reactions?.dislikes || 0}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export interface PostsTableProps {
+ posts: Post[];
+ selectedTag?: string;
+ makeTitleSegments: (title: string) => HighlightSegment[];
+ onClickTag: (tag: string) => void;
+ onOpenUser: (user: Post["author"]) => void;
+ onOpenDetail: (post: Post) => void;
+ onEdit: (post: Post) => void;
+ onDelete: (id: number) => void;
+}
+
+export function PostsTable({
+ posts,
+ selectedTag,
+ makeTitleSegments,
+ onClickTag,
+ onOpenUser,
+ onOpenDetail,
+ onEdit,
+ onDelete,
+}: Readonly) {
+ return (
+
+
+
+ ID
+ 제목
+ 작성자
+ 반응
+ 작업
+
+
+
+ {posts.map((post) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/entities/user/api/user.api.ts b/src/entities/user/api/user.api.ts
new file mode 100644
index 000000000..dc55ebc88
--- /dev/null
+++ b/src/entities/user/api/user.api.ts
@@ -0,0 +1,21 @@
+import { http } from "@/shared/api/client";
+
+import { User } from "../model/user.types";
+
+export type UserLite = Pick;
+
+interface GetUserLiteResponse {
+ users: UserLite[];
+}
+
+export const userApi = {
+ async getProfile(): Promise {
+ const data = await http.get("/users", {
+ params: { limit: 0, select: ["username", "image"] },
+ });
+ return data.users;
+ },
+ async getById(id: number): Promise {
+ return http.get(`/users/${id}`);
+ },
+} as const;
diff --git a/src/entities/user/index.ts b/src/entities/user/index.ts
new file mode 100644
index 000000000..b105432e2
--- /dev/null
+++ b/src/entities/user/index.ts
@@ -0,0 +1,5 @@
+export type { User } from "./model/user.types";
+export { userApi } from "./api/user.api";
+export { currentUserIdAtom, selectedUserAtom, isUserModalOpenAtom } from "./model/user.atom";
+export { useSelectedUser } from "./model/user.hook";
+export { UserDetailDialog } from "./ui/user-detail-dialog";
diff --git a/src/entities/user/model/user.atom.ts b/src/entities/user/model/user.atom.ts
new file mode 100644
index 000000000..cc7ee3147
--- /dev/null
+++ b/src/entities/user/model/user.atom.ts
@@ -0,0 +1,8 @@
+import { atom } from "jotai";
+
+export const currentUserIdAtom = atom(null);
+
+import type { User } from "./user.types";
+
+export const selectedUserAtom = atom(null);
+export const isUserModalOpenAtom = atom(false);
diff --git a/src/entities/user/model/user.hook.ts b/src/entities/user/model/user.hook.ts
new file mode 100644
index 000000000..dc5ad94b1
--- /dev/null
+++ b/src/entities/user/model/user.hook.ts
@@ -0,0 +1,26 @@
+import { useAtom } from "jotai";
+
+import { isUserModalOpenAtom, selectedUserAtom } from "./user.atom";
+import { userApi } from "../api/user.api";
+
+export function useSelectedUser() {
+ const [selectedUser, setSelectedUser] = useAtom(selectedUserAtom);
+ const [isUserModalOpen, setIsUserModalOpen] = useAtom(isUserModalOpenAtom);
+
+ const closeUserModal = () => setIsUserModalOpen(false);
+
+ const openUserModal = async (id: number) => {
+ const user = await userApi.getById(id);
+ setSelectedUser(user);
+ setIsUserModalOpen(true);
+ };
+
+ return {
+ selectedUser,
+ setSelectedUser,
+ isUserModalOpen,
+ setIsUserModalOpen,
+ openUserModal,
+ closeUserModal,
+ } as const;
+}
diff --git a/src/entities/user/model/user.query.ts b/src/entities/user/model/user.query.ts
new file mode 100644
index 000000000..ed54cc320
--- /dev/null
+++ b/src/entities/user/model/user.query.ts
@@ -0,0 +1,25 @@
+import { useQuery, type QueryClient } from "@tanstack/react-query";
+
+import { userApi } from "@/entities/user";
+
+import type { User } from "./user.types";
+
+export const userQueryKeys = {
+ all: ["users"] as const,
+ byId: (id: number) => ["users", id] as const,
+} as const;
+
+export function useUserQuery(userId?: number | null) {
+ return useQuery({
+ enabled: !!userId,
+ queryKey: userId ? userQueryKeys.byId(userId) : userQueryKeys.all,
+ queryFn: () => userApi.getById(userId as number),
+ staleTime: 60_000,
+ });
+}
+
+export const prefetchUserById = (client: QueryClient, id: number) =>
+ client.prefetchQuery({ queryKey: userQueryKeys.byId(id), queryFn: () => userApi.getById(id) });
+
+export const getUserFromCache = (client: QueryClient, id: number): User | undefined =>
+ client.getQueryData(userQueryKeys.byId(id));
diff --git a/src/entities/user/model/user.types.ts b/src/entities/user/model/user.types.ts
new file mode 100644
index 000000000..1389872fe
--- /dev/null
+++ b/src/entities/user/model/user.types.ts
@@ -0,0 +1,19 @@
+export interface User {
+ id: number;
+ username: string;
+ image?: string;
+ firstName?: string;
+ lastName?: string;
+ age?: number;
+ email?: string;
+ phone?: string;
+ address?: {
+ address?: string;
+ city?: string;
+ state?: string;
+ };
+ company?: {
+ name?: string;
+ title?: string;
+ };
+}
diff --git a/src/entities/user/ui/user-detail-dialog.tsx b/src/entities/user/ui/user-detail-dialog.tsx
new file mode 100644
index 000000000..e5b08cd60
--- /dev/null
+++ b/src/entities/user/ui/user-detail-dialog.tsx
@@ -0,0 +1,45 @@
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@shared/ui";
+
+import type { User } from "../model/user.types";
+
+interface UserDetailDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ user: User | null;
+}
+
+export function UserDetailDialog({ open, onOpenChange, user }: Readonly) {
+ return (
+
+ );
+}
diff --git a/src/features/comment-edit/index.ts b/src/features/comment-edit/index.ts
new file mode 100644
index 000000000..e7c1eb757
--- /dev/null
+++ b/src/features/comment-edit/index.ts
@@ -0,0 +1,5 @@
+export { useCommentEditor, useNewCommentForm, useEditCommentDialog } from "./model/edit-comment.hook";
+export type { NewCommentDraft } from "./model/edit-comment.hook";
+export { CommentAddDialogContainer } from "./ui/comment-add-dialog-container";
+export { CommentEditDialogContainer } from "./ui/comment-edit-dialog-container";
+export { CommentsListContainer } from "./ui/comments-list-container";
diff --git a/src/features/comment-edit/model/edit-comment.atoms.ts b/src/features/comment-edit/model/edit-comment.atoms.ts
new file mode 100644
index 000000000..1b5f977a0
--- /dev/null
+++ b/src/features/comment-edit/model/edit-comment.atoms.ts
@@ -0,0 +1,14 @@
+import { atom } from "jotai";
+
+export interface NewCommentDraft {
+ body: string;
+ postId: number | null;
+ userId: number;
+}
+
+const defaultCommentDraft: NewCommentDraft = { body: "", postId: null, userId: 1 };
+
+export const newCommentAtom = atom(defaultCommentDraft);
+
+export const isAddCommentDialogOpenAtom = atom(false);
+export const isEditCommentDialogOpenAtom = atom(false);
diff --git a/src/features/comment-edit/model/edit-comment.hook.ts b/src/features/comment-edit/model/edit-comment.hook.ts
new file mode 100644
index 000000000..4be64abd2
--- /dev/null
+++ b/src/features/comment-edit/model/edit-comment.hook.ts
@@ -0,0 +1,112 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useAtom } from "jotai";
+import { useState } from "react";
+
+import {
+ useCreateCommentMutation,
+ useUpdateCommentMutation,
+ useDeleteCommentMutation,
+ useLikeCommentMutation,
+ useComments,
+} from "@/entities/comment";
+import type { CreateCommentPayload, UpdateCommentPayload, Comment } from "@/entities/comment";
+import { commentQueryKeys } from "@/entities/comment/model/comment.keys";
+
+import {
+ isAddCommentDialogOpenAtom,
+ isEditCommentDialogOpenAtom,
+ newCommentAtom,
+ type NewCommentDraft,
+} from "./edit-comment.atoms";
+
+export function useCommentEditor() {
+ const queryClient = useQueryClient();
+ const createMutation = useCreateCommentMutation();
+ const deleteMutation = useDeleteCommentMutation();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [newComment, setNewComment] = useAtom(newCommentAtom);
+ const [isAddOpen, setIsAddOpen] = useAtom(isAddCommentDialogOpenAtom);
+ const [isEditOpen, setIsEditOpen] = useAtom(isEditCommentDialogOpenAtom);
+ const updateMutation = useUpdateCommentMutation();
+ const likeMutation = useLikeCommentMutation();
+ const { selectedComment } = useComments();
+
+ const addComment = async (payload: CreateCommentPayload) => {
+ setIsSubmitting(true);
+ try {
+ await createMutation.mutateAsync(payload);
+ setNewComment({ body: "", postId: null, userId: newComment.userId });
+ setIsAddOpen(false);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const updateComment = async (payload: UpdateCommentPayload) => {
+ setIsSubmitting(true);
+ try {
+ const postId = selectedComment?.postId ?? 0;
+ await updateMutation.mutateAsync({ postId, id: payload.id, body: payload.body });
+ setIsEditOpen(false);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const deleteComment = async (id: number, postId: number) => {
+ setIsSubmitting(true);
+ try {
+ await deleteMutation.mutateAsync({ id, postId });
+ setIsEditOpen(false);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const likeComment = async (id: number, postId: number) => {
+ const list = queryClient.getQueryData(commentQueryKeys.byPost(postId)) ?? [];
+ const currentLikes = list.find((c) => c.id === id)?.likes ?? 0;
+ setIsSubmitting(true);
+ try {
+ await likeMutation.mutateAsync({ id, postId, likes: currentLikes + 1 });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const prepareNewComment = (postId: number, userId?: number) => {
+ setNewComment((prev) => ({ ...prev, postId, userId: userId ?? prev.userId }));
+ setIsAddOpen(true);
+ };
+
+ const resetDraft = () => setNewComment((prev) => ({ ...prev, body: "", postId: null }));
+
+ return {
+ addComment,
+ updateComment,
+ deleteComment,
+ likeComment,
+ isSubmitting,
+ newComment,
+ setNewComment,
+ isAddOpen,
+ setIsAddOpen,
+ isEditOpen,
+ setIsEditOpen,
+ prepareNewComment,
+ resetDraft,
+ };
+}
+
+export function useNewCommentForm() {
+ const [newComment, setNewComment] = useAtom(newCommentAtom);
+ const [isAddOpen, setIsAddOpen] = useAtom(isAddCommentDialogOpenAtom);
+ return { newComment, setNewComment, isAddOpen, setIsAddOpen } as const;
+}
+
+export function useEditCommentDialog() {
+ const [isEditOpen, setIsEditOpen] = useAtom(isEditCommentDialogOpenAtom);
+ return { isEditOpen, setIsEditOpen } as const;
+}
+
+export type { NewCommentDraft };
diff --git a/src/features/comment-edit/ui/comment-add-dialog-container.tsx b/src/features/comment-edit/ui/comment-add-dialog-container.tsx
new file mode 100644
index 000000000..15744f4ee
--- /dev/null
+++ b/src/features/comment-edit/ui/comment-add-dialog-container.tsx
@@ -0,0 +1,22 @@
+import { CommentAddDialog } from "@/entities/comment";
+
+import { useCommentEditor } from "../model/edit-comment.hook";
+
+export function CommentAddDialogContainer() {
+ const { newComment, setNewComment, isAddOpen, setIsAddOpen, addComment } = useCommentEditor();
+
+ const handleSubmit = async () => {
+ if (newComment.postId == null) return;
+ await addComment({ body: newComment.body, postId: newComment.postId, userId: newComment.userId });
+ };
+
+ return (
+ setNewComment((prev) => ({ ...prev, body: e.currentTarget.value }))}
+ onSubmit={handleSubmit}
+ />
+ );
+}
diff --git a/src/features/comment-edit/ui/comment-edit-dialog-container.tsx b/src/features/comment-edit/ui/comment-edit-dialog-container.tsx
new file mode 100644
index 000000000..b0224c1fe
--- /dev/null
+++ b/src/features/comment-edit/ui/comment-edit-dialog-container.tsx
@@ -0,0 +1,23 @@
+import { CommentEditDialog, useComments } from "@/entities/comment";
+
+import { useCommentEditor } from "../model/edit-comment.hook";
+
+export function CommentEditDialogContainer() {
+ const { isEditOpen, setIsEditOpen, updateComment } = useCommentEditor();
+ const { selectedComment, setSelectedComment } = useComments();
+
+ const handleSubmit = async () => {
+ if (!selectedComment) return;
+ await updateComment({ id: selectedComment.id, body: selectedComment.body });
+ };
+
+ return (
+ setSelectedComment((prev) => (prev ? { ...prev, body: e.currentTarget.value } : prev))}
+ onSubmit={handleSubmit}
+ />
+ );
+}
diff --git a/src/features/comment-edit/ui/comments-list-container.tsx b/src/features/comment-edit/ui/comments-list-container.tsx
new file mode 100644
index 000000000..dbb7825b7
--- /dev/null
+++ b/src/features/comment-edit/ui/comments-list-container.tsx
@@ -0,0 +1,52 @@
+import { useAtom } from "jotai";
+import { Plus } from "lucide-react";
+import { useCallback } from "react";
+
+import { CommentList, useCommentsQuery } from "@/entities/comment";
+import type { Comment } from "@/entities/comment";
+import { selectedCommentAtom } from "@/entities/comment/model/comment.atom";
+import { Button } from "@/shared/ui/button";
+
+import { useEditCommentDialog } from "../index";
+import { useCommentEditor } from "../model/edit-comment.hook";
+
+interface CommentsListContainerProps {
+ postId?: number;
+ searchQuery: string;
+}
+
+export function CommentsListContainer({ postId, searchQuery }: Readonly) {
+ const { data: comments = [] } = useCommentsQuery(postId);
+ const [, setSelectedComment] = useAtom(selectedCommentAtom);
+ const { setIsEditOpen } = useEditCommentDialog();
+ const { deleteComment, likeComment, prepareNewComment } = useCommentEditor();
+
+ const handleEditComment = useCallback(
+ (newComment: Comment) => {
+ setSelectedComment(newComment);
+ setIsEditOpen(true);
+ },
+ [setSelectedComment, setIsEditOpen],
+ );
+
+ if (!postId) return null;
+
+ return (
+
+
+
댓글
+
+
+
likeComment(id, postId)}
+ onEdit={handleEditComment}
+ onDelete={(id) => deleteComment(id, postId)}
+ />
+
+ );
+}
diff --git a/src/features/post-edit/index.ts b/src/features/post-edit/index.ts
new file mode 100644
index 000000000..b173d13da
--- /dev/null
+++ b/src/features/post-edit/index.ts
@@ -0,0 +1,5 @@
+export { usePostEditor } from "./model/edit-post.hook";
+export { useNewPostForm, useEditPostDialog } from "./model/edit-post.hook";
+export type { NewPostDraft } from "./model/edit-post.atoms";
+export { PostAddDialogContainer } from "./ui/post-add-dialog-container";
+export { PostEditDialogContainer } from "./ui/post-edit-dialog-container";
diff --git a/src/features/post-edit/model/edit-post.atoms.ts b/src/features/post-edit/model/edit-post.atoms.ts
new file mode 100644
index 000000000..ac0aee417
--- /dev/null
+++ b/src/features/post-edit/model/edit-post.atoms.ts
@@ -0,0 +1,7 @@
+import { atom } from "jotai";
+
+export type NewPostDraft = { title: string; body: string; userId: number };
+
+export const newPostAtom = atom({ title: "", body: "", userId: 1 });
+export const isAddPostDialogOpenAtom = atom(false);
+export const isEditPostDialogOpenAtom = atom(false);
diff --git a/src/features/post-edit/model/edit-post.hook.ts b/src/features/post-edit/model/edit-post.hook.ts
new file mode 100644
index 000000000..35a545740
--- /dev/null
+++ b/src/features/post-edit/model/edit-post.hook.ts
@@ -0,0 +1,54 @@
+import { useAtom } from "jotai";
+import { useState } from "react";
+
+import type { CreatePostParams, UpdatePostPayload } from "@/entities/post";
+import { useCreatePostMutation, useDeletePostMutation, useUpdatePostMutation } from "@/entities/post/model/post.query";
+
+import { isAddPostDialogOpenAtom, isEditPostDialogOpenAtom, newPostAtom } from "./edit-post.atoms";
+
+export const useNewPostForm = () => {
+ const [newPost, setNewPost] = useAtom(newPostAtom);
+ const [isAddOpen, setIsAddOpen] = useAtom(isAddPostDialogOpenAtom);
+ return { newPost, setNewPost, isAddOpen, setIsAddOpen } as const;
+};
+
+export const useEditPostDialog = () => {
+ const [isEditOpen, setIsEditOpen] = useAtom(isEditPostDialogOpenAtom);
+ return { isEditOpen, setIsEditOpen } as const;
+};
+
+export const usePostEditor = () => {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const createMutation = useCreatePostMutation();
+ const updateMutation = useUpdatePostMutation();
+ const deleteMutation = useDeletePostMutation();
+
+ const addPost = async (payload: CreatePostParams) => {
+ setIsSubmitting(true);
+ try {
+ await createMutation.mutateAsync(payload);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const updatePost = async (payload: UpdatePostPayload) => {
+ setIsSubmitting(true);
+ try {
+ await updateMutation.mutateAsync(payload);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const deletePost = async (id: number) => {
+ setIsSubmitting(true);
+ try {
+ await deleteMutation.mutateAsync(id);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return { addPost, updatePost, deletePost, isSubmitting };
+};
diff --git a/src/features/post-edit/ui/post-add-dialog-container.tsx b/src/features/post-edit/ui/post-add-dialog-container.tsx
new file mode 100644
index 000000000..fffa5e09e
--- /dev/null
+++ b/src/features/post-edit/ui/post-add-dialog-container.tsx
@@ -0,0 +1,27 @@
+import { PostAddDialog } from "@/entities/post";
+
+import { useNewPostForm, usePostEditor } from "../model/edit-post.hook";
+
+export function PostAddDialogContainer() {
+ const { newPost, setNewPost, isAddOpen, setIsAddOpen } = useNewPostForm();
+ const { addPost } = usePostEditor();
+
+ const handleSubmit = async () => {
+ try {
+ await addPost(newPost);
+ } finally {
+ setIsAddOpen(false);
+ }
+ setNewPost({ title: "", body: "", userId: newPost.userId });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/features/post-edit/ui/post-edit-dialog-container.tsx b/src/features/post-edit/ui/post-edit-dialog-container.tsx
new file mode 100644
index 000000000..07a1994ce
--- /dev/null
+++ b/src/features/post-edit/ui/post-edit-dialog-container.tsx
@@ -0,0 +1,32 @@
+import { PostEditDialog } from "@/entities/post";
+import { usePosts } from "@/entities/post";
+
+import { useEditPostDialog, usePostEditor } from "../model/edit-post.hook";
+
+export function PostEditDialogContainer() {
+ const { isEditOpen, setIsEditOpen } = useEditPostDialog();
+ const { updatePost } = usePostEditor();
+ const { selectedPost, setSelectedPost } = usePosts();
+
+ const handleSubmit = async () => {
+ if (!selectedPost) return;
+ try {
+ await updatePost({
+ postId: String(selectedPost.id),
+ params: { title: selectedPost.title ?? "", body: selectedPost.body ?? "" },
+ });
+ } finally {
+ setIsEditOpen(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/features/post-filter/index.ts b/src/features/post-filter/index.ts
new file mode 100644
index 000000000..f2eeb3c33
--- /dev/null
+++ b/src/features/post-filter/index.ts
@@ -0,0 +1,3 @@
+export { usePostSearchParams } from "./model/filter-post.hook";
+export type { SortBy, SortOrder } from "./model/filter-post.atoms";
+export { useTagsQuery } from "./model/use-tags.query";
diff --git a/src/features/post-filter/model/filter-post.atoms.ts b/src/features/post-filter/model/filter-post.atoms.ts
new file mode 100644
index 000000000..6d0f9692d
--- /dev/null
+++ b/src/features/post-filter/model/filter-post.atoms.ts
@@ -0,0 +1,14 @@
+import { atom } from "jotai";
+
+import { Tag } from "@/entities/post/model/post.types";
+
+export type SortBy = "none" | "id" | "title" | "reactions";
+export type SortOrder = "asc" | "desc";
+
+export const skipAtom = atom(0);
+export const limitAtom = atom(10);
+export const searchQueryAtom = atom("");
+export const sortByAtom = atom("none");
+export const sortOrderAtom = atom("asc");
+export const tagsAtom = atom([]);
+export const selectedTagAtom = atom("");
diff --git a/src/features/post-filter/model/filter-post.hook.ts b/src/features/post-filter/model/filter-post.hook.ts
new file mode 100644
index 000000000..82e91cb68
--- /dev/null
+++ b/src/features/post-filter/model/filter-post.hook.ts
@@ -0,0 +1,49 @@
+import { useCallback, useMemo } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+
+export interface PostSearchParams {
+ skip: number;
+ limit: number;
+ search?: string;
+ sortBy?: string;
+ sortOrder?: string;
+ tag?: string;
+}
+
+export const usePostSearchParams = () => {
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ const value: PostSearchParams = useMemo(() => {
+ const params = new URLSearchParams(location.search);
+ return {
+ skip: Number.parseInt(params.get("skip") || "0") || 0,
+ limit: Number.parseInt(params.get("limit") || "10") || 10,
+ search: params.get("search") || undefined,
+ sortBy: params.get("sortBy") || "id",
+ sortOrder: params.get("sortOrder") || "asc",
+ tag: params.get("tag") || undefined,
+ };
+ }, [location.search]);
+
+ const update = useCallback(
+ (next: Partial) => {
+ const merged: PostSearchParams = { ...value, sortBy: "id", sortOrder: "asc", ...next } as PostSearchParams;
+ const current = new URLSearchParams(location.search);
+ current.set("skip", String(merged.skip ?? 0));
+ current.set("limit", String(merged.limit ?? 10));
+ const setOrDelete = (key: string, v?: string) => {
+ if (v && v.length) current.set(key, v);
+ else current.delete(key);
+ };
+ setOrDelete("search", merged.search);
+ current.set("sortBy", merged.sortBy || "id");
+ current.set("sortOrder", merged.sortOrder || "asc");
+ setOrDelete("tag", merged.tag);
+ navigate({ search: `?${current.toString()}` }, { replace: false });
+ },
+ [location.search, navigate, value],
+ );
+
+ return { params: value, setParams: update } as const;
+};
diff --git a/src/features/post-filter/model/use-tags.query.ts b/src/features/post-filter/model/use-tags.query.ts
new file mode 100644
index 000000000..46852fc6e
--- /dev/null
+++ b/src/features/post-filter/model/use-tags.query.ts
@@ -0,0 +1,12 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { postApi } from "@/entities/post";
+import { postsQueryKeys } from "@/entities/post/model/post.keys";
+
+export function useTagsQuery() {
+ return useQuery({
+ queryKey: postsQueryKeys.tags,
+ queryFn: () => postApi.getTags(),
+ staleTime: 5 * 60_000,
+ });
+}
diff --git a/src/features/post-filter/ui/post-filter-container.tsx b/src/features/post-filter/ui/post-filter-container.tsx
new file mode 100644
index 000000000..3c6f53258
--- /dev/null
+++ b/src/features/post-filter/ui/post-filter-container.tsx
@@ -0,0 +1,57 @@
+import { ChangeEventHandler, useCallback } from "react";
+
+import { PostFilter } from "@entities/post/ui/post-filter";
+
+import { SortBy, SortOrder, usePostSearchParams, useTagsQuery } from "@features/post-filter";
+
+export function PostFilterContainer() {
+ const { params, setParams } = usePostSearchParams();
+ const searchQuery = params.search ?? "";
+ const selectedTag = params.tag ?? "";
+ const sortBy = (params.sortBy as SortBy) ?? "id";
+ const sortOrder = (params.sortOrder as SortOrder) ?? "asc";
+ const { data: tags = [] } = useTagsQuery();
+
+ const handleSearchQueryChange: ChangeEventHandler = (event) =>
+ setParams({ search: event.target.value, skip: 0 });
+
+ const handleEnter = useCallback(() => {
+ setParams({ search: searchQuery?.trim() || undefined, skip: 0 });
+ }, [searchQuery, setParams]);
+
+ const handleChangeTag = useCallback(
+ (value: string) => {
+ setParams({ tag: value || undefined, skip: 0 });
+ },
+ [setParams],
+ );
+
+ const handleChangeSortBy = useCallback(
+ (value: string) => {
+ setParams({ sortBy: value as SortBy, skip: 0 });
+ },
+ [setParams],
+ );
+
+ const handleChangeSortOrder = useCallback(
+ (value: string) => {
+ setParams({ sortOrder: value as SortOrder, skip: 0 });
+ },
+ [setParams],
+ );
+
+ return (
+
+ );
+}
diff --git a/src/features/post-load/api/post-load.api.ts b/src/features/post-load/api/post-load.api.ts
new file mode 100644
index 000000000..f108264a8
--- /dev/null
+++ b/src/features/post-load/api/post-load.api.ts
@@ -0,0 +1,59 @@
+import { postApi, type PostsParams } from "@/entities/post";
+import type { Post, PostsResponse } from "@/entities/post";
+import { userApi } from "@/entities/user";
+
+export const postLoadApi = {
+ async getWithAuthors(params: PostsParams): Promise {
+ const [{ posts, total }, users] = await Promise.all([postApi.get(params), userApi.getProfile()]);
+ const postsWithUsers: Post[] = posts.map((post) => ({
+ ...post,
+ author: users.find((user) => user.id === post.userId),
+ }));
+ return { posts: sortPosts(postsWithUsers, params.sortBy, params.order), total };
+ },
+ async getByTagWithAuthors(
+ tag: string,
+ params?: Pick,
+ ): Promise {
+ const [{ posts, total }, users] = await Promise.all([postApi.getByTag(tag, params), userApi.getProfile()]);
+ const postsWithUsers: Post[] = posts.map((post) => ({
+ ...post,
+ author: users.find((user) => user.id === post.userId),
+ }));
+ return { posts: sortPosts(postsWithUsers, params?.sortBy, params?.order), total };
+ },
+ async searchWithAuthors(
+ query: string,
+ params?: Pick
+ ): Promise {
+ const [{ posts, total }, users] = await Promise.all([postApi.search(query, params), userApi.getProfile()]);
+ const postsWithUsers: Post[] = posts.map((post) => ({
+ ...post,
+ author: users.find((user) => user.id === post.userId),
+ }));
+ return { posts: sortPosts(postsWithUsers, params?.sortBy, params?.order), total };
+ },
+} as const;
+
+function sortPosts(posts: Post[], sortBy?: string, order: "asc" | "desc" = "asc"): Post[] {
+ if (!sortBy || sortBy === "none") return posts;
+ const dir = order === "desc" ? -1 : 1;
+ const sorted = [...posts].sort((a, b) => {
+ switch (sortBy) {
+ case "id":
+ return (a.id - b.id) * dir;
+ case "title":
+ return (a.title ?? "").localeCompare(b.title ?? "") * dir;
+ case "userId":
+ return ((a.userId ?? 0) - (b.userId ?? 0)) * dir;
+ case "reactions": {
+ const la = a.reactions?.likes ?? 0;
+ const lb = b.reactions?.likes ?? 0;
+ return (la - lb) * dir;
+ }
+ default:
+ return 0;
+ }
+ });
+ return sorted;
+}
diff --git a/src/features/post-load/index.ts b/src/features/post-load/index.ts
new file mode 100644
index 000000000..d47ad5590
--- /dev/null
+++ b/src/features/post-load/index.ts
@@ -0,0 +1,3 @@
+export { postLoadApi } from "./api/post-load.api";
+export type { PostsParams, PostsResponse } from "@/entities/post";
+export { PostsTableContainer } from "../post-load/ui/posts-table-container";
diff --git a/src/features/post-load/model/post-load.query.ts b/src/features/post-load/model/post-load.query.ts
new file mode 100644
index 000000000..13a6d3122
--- /dev/null
+++ b/src/features/post-load/model/post-load.query.ts
@@ -0,0 +1,59 @@
+import type { PostsParams, PostsResponse } from "@/entities/post";
+import { postsQueryKeys } from "@/entities/post/model/post.keys";
+
+import { postLoadApi } from "../api/post-load.api";
+
+import type { QueryClient } from "@tanstack/react-query";
+
+export const postsListQuery = (params: PostsParams) => ({
+ queryKey: postsQueryKeys.list({
+ limit: params.limit,
+ skip: params.skip,
+ sortBy: params.sortBy,
+ order: params.order,
+ }),
+ queryFn: (): Promise => postLoadApi.getWithAuthors(params),
+});
+
+export const postsByTagQuery = (tag: string, params?: Pick) => ({
+ queryKey: postsQueryKeys.byTag(tag, params),
+ queryFn: (): Promise => postLoadApi.getByTagWithAuthors(tag, params),
+});
+
+export const postsSearchQuery = (query: string, params?: Pick) => ({
+ queryKey: postsQueryKeys.search(query, params),
+ queryFn: (): Promise => postLoadApi.searchWithAuthors(query, params),
+});
+
+export type BuildPostsQueryParams = {
+ limit?: number;
+ skip?: number;
+ tag?: string;
+ search?: string;
+ sortBy?: string;
+ sortOrder?: "asc" | "desc";
+};
+
+export const buildPostsQuery = (params: BuildPostsQueryParams) => {
+ const search = params.search?.trim();
+ const normalizedSortBy = params.sortBy && params.sortBy !== "none" ? params.sortBy : undefined;
+ const sortParams = normalizedSortBy ? { sortBy: normalizedSortBy, order: params.sortOrder } : undefined;
+ if (search) return postsSearchQuery(search, { ...sortParams, limit: params.limit ?? 0, skip: params.skip ?? 0 });
+ if (params.tag)
+ return postsByTagQuery(params.tag, { ...sortParams, limit: params.limit ?? 0, skip: params.skip ?? 0 });
+ if (typeof params.limit === "number" && typeof params.skip === "number") {
+ return postsListQuery({
+ limit: params.limit,
+ skip: params.skip,
+ sortBy: normalizedSortBy,
+ order: params.sortOrder,
+ });
+ }
+ return postsListQuery({ limit: 0, skip: 0, sortBy: normalizedSortBy, order: params.sortOrder });
+};
+
+export const useGetPostList = (client: QueryClient, params: PostsParams) => client.fetchQuery(postsListQuery(params));
+
+export const useGetPostsByTag = (client: QueryClient, tag: string) => client.fetchQuery(postsByTagQuery(tag));
+
+export const useSearchPosts = (client: QueryClient, query: string) => client.fetchQuery(postsSearchQuery(query));
diff --git a/src/features/post-load/model/posts.query.ts b/src/features/post-load/model/posts.query.ts
new file mode 100644
index 000000000..7f06f7fac
--- /dev/null
+++ b/src/features/post-load/model/posts.query.ts
@@ -0,0 +1,14 @@
+import { useQuery } from "@tanstack/react-query";
+
+import type { PostsResponse } from "@/entities/post";
+
+import { buildPostsQuery, type BuildPostsQueryParams } from "./post-load.query";
+
+export function usePostsQuery(params: BuildPostsQueryParams) {
+ const query = useQuery({
+ ...buildPostsQuery(params),
+ staleTime: 30_000,
+ gcTime: 5 * 60_000,
+ });
+ return query;
+}
diff --git a/src/features/post-load/ui/posts-table-container.tsx b/src/features/post-load/ui/posts-table-container.tsx
new file mode 100644
index 000000000..fa40f3da2
--- /dev/null
+++ b/src/features/post-load/ui/posts-table-container.tsx
@@ -0,0 +1,78 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useCallback } from "react";
+
+import { prefetchCommentsByPost } from "@/entities/comment/model/comment.query";
+import { PostsTable, usePosts } from "@/entities/post";
+import type { Post } from "@/entities/post";
+import { useEditPostDialog, usePostEditor } from "@/features/post-edit";
+import type { SortOrder } from "@/features/post-filter";
+import { usePostSearchParams } from "@/features/post-filter/model/filter-post.hook";
+import { usePostsQuery } from "@/features/post-load/model/posts.query.ts";
+import { useUserDetailModal } from "@/features/user-load";
+import { splitByHighlight } from "@/shared/lib/split-by-highlight";
+
+export function PostsTableContainer() {
+ const { setSelectedPost, setIsDetailOpen } = usePosts();
+ const { params, setParams } = usePostSearchParams();
+ const selectedTag = params.tag ?? "";
+ const searchQuery = params.search ?? "";
+ const { data } = usePostsQuery({
+ limit: params.limit,
+ skip: params.skip,
+ tag: params.tag,
+ search: params.search,
+ sortBy: params.sortBy,
+ sortOrder: (params.sortOrder as SortOrder) ?? "asc",
+ });
+ const posts: Post[] = data?.posts ?? [];
+ const { openById } = useUserDetailModal();
+ const { setIsEditOpen } = useEditPostDialog();
+ const { deletePost } = usePostEditor();
+ const queryClient = useQueryClient();
+
+ const handleClickTag = useCallback(
+ (tag: string) => {
+ if (tag === selectedTag) return;
+ setParams({ tag, skip: 0 });
+ },
+ [selectedTag, setParams],
+ );
+
+ const handleOpenUser = useCallback(
+ (user: Post["author"]) => {
+ if (!user) return;
+ void openById(user.id);
+ },
+ [openById],
+ );
+
+ const handleOpenDetail = useCallback(
+ async (post: Post) => {
+ setSelectedPost(post);
+ await prefetchCommentsByPost(queryClient, post.id);
+ setIsDetailOpen(true);
+ },
+ [queryClient, setIsDetailOpen, setSelectedPost],
+ );
+
+ const handleEditPost = useCallback(
+ (post: Post) => {
+ setSelectedPost(post);
+ setIsEditOpen(true);
+ },
+ [setIsEditOpen, setSelectedPost],
+ );
+
+ return (
+ splitByHighlight(title, searchQuery)}
+ onClickTag={handleClickTag}
+ onOpenUser={handleOpenUser}
+ onOpenDetail={handleOpenDetail}
+ onEdit={handleEditPost}
+ onDelete={deletePost}
+ />
+ );
+}
diff --git a/src/features/post-pagination/ui/post-pagination.tsx b/src/features/post-pagination/ui/post-pagination.tsx
new file mode 100644
index 000000000..0aebc7710
--- /dev/null
+++ b/src/features/post-pagination/ui/post-pagination.tsx
@@ -0,0 +1,40 @@
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/ui";
+import { Button } from "@/shared/ui/button";
+
+export interface PostPaginationProps {
+ total: number;
+ skip: number;
+ limit: number;
+ onChangeLimit: (value: number) => void;
+ onPrev: () => void;
+ onNext: () => void;
+}
+
+export function PostPagination({ total, skip, limit, onChangeLimit, onPrev, onNext }: Readonly) {
+ return (
+
+
+ 표시
+
+ 항목
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/user-load/index.ts b/src/features/user-load/index.ts
new file mode 100644
index 000000000..c8d5d2919
--- /dev/null
+++ b/src/features/user-load/index.ts
@@ -0,0 +1,2 @@
+export { useUserDetailModal } from "./model/user-detail-modal.hook";
+export { UserDetailDialogContainer } from "./ui/user-detail-dialog-container";
diff --git a/src/features/user-load/model/user-detail-modal.atoms.ts b/src/features/user-load/model/user-detail-modal.atoms.ts
new file mode 100644
index 000000000..073a6aa7f
--- /dev/null
+++ b/src/features/user-load/model/user-detail-modal.atoms.ts
@@ -0,0 +1,6 @@
+import { atom } from "jotai";
+
+import type { User } from "@/entities/user";
+
+export const isUserDetailOpenAtom = atom(false);
+export const selectedUserAtom = atom(null);
diff --git a/src/features/user-load/model/user-detail-modal.hook.ts b/src/features/user-load/model/user-detail-modal.hook.ts
new file mode 100644
index 000000000..68606e56b
--- /dev/null
+++ b/src/features/user-load/model/user-detail-modal.hook.ts
@@ -0,0 +1,24 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useAtom } from "jotai";
+
+import { prefetchUserById, getUserFromCache } from "@/entities/user/model/user.query";
+
+import { isUserDetailOpenAtom, selectedUserAtom } from "./user-detail-modal.atoms";
+
+export function useUserDetailModal() {
+ const [isOpen, setIsOpen] = useAtom(isUserDetailOpenAtom);
+ const [user, setUser] = useAtom(selectedUserAtom);
+ const queryClient = useQueryClient();
+
+ const openById = async (id: number) => {
+ const cached = getUserFromCache(queryClient, id);
+ if (cached) setUser(cached);
+ await prefetchUserById(queryClient, id);
+ setUser(getUserFromCache(queryClient, id) ?? cached ?? null);
+ setIsOpen(true);
+ };
+
+ const close = () => setIsOpen(false);
+
+ return { isOpen, setIsOpen, user, setUser, openById, close } as const;
+}
diff --git a/src/features/user-load/ui/user-detail-dialog-container.tsx b/src/features/user-load/ui/user-detail-dialog-container.tsx
new file mode 100644
index 000000000..4f53617b1
--- /dev/null
+++ b/src/features/user-load/ui/user-detail-dialog-container.tsx
@@ -0,0 +1,8 @@
+import { UserDetailDialog } from "@/entities/user";
+
+import { useUserDetailModal } from "../model/user-detail-modal.hook";
+
+export function UserDetailDialogContainer() {
+ const { isOpen, setIsOpen, user } = useUserDetailModal();
+ return ;
+}
diff --git a/src/index.css b/src/index.css
index 62d46a326..8e1047cca 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,4 +1,5 @@
-html, body {
+html,
+body {
background: #fff;
color: #000;
-}
\ No newline at end of file
+}
diff --git a/src/index.tsx b/src/index.tsx
index 369e197bb..be485486d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,7 +1,7 @@
-import React from "react"
-import ReactDOM from "react-dom/client"
-import { BrowserRouter as Router } from "react-router-dom"
-import App from "./App"
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { BrowserRouter as Router } from "react-router-dom";
+import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
@@ -9,4 +9,4 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
,
-)
+);
diff --git a/src/main.tsx b/src/main.tsx
index bef5202a3..5e6b0cde3 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,15 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
-createRoot(document.getElementById('root')!).render(
+import "@/index.css";
+import { QueryProvider } from "@/app/ui/query-provider";
+
+import App from "./App.tsx";
+
+createRoot(document.getElementById("root")!).render(
-
+
+
+
,
-)
+);
diff --git a/src/pages/PostsManagerPage.tsx b/src/pages/PostsManagerPage.tsx
deleted file mode 100644
index f80eb91ef..000000000
--- a/src/pages/PostsManagerPage.tsx
+++ /dev/null
@@ -1,708 +0,0 @@
-import { useEffect, useState } from "react"
-import { Edit2, MessageSquare, Plus, Search, ThumbsDown, ThumbsUp, Trash2 } from "lucide-react"
-import { useLocation, useNavigate } from "react-router-dom"
-import {
- Button,
- Card,
- CardContent,
- CardHeader,
- CardTitle,
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- Input,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- Textarea,
-} from "../components"
-
-const PostsManager = () => {
- const navigate = useNavigate()
- const location = useLocation()
- const queryParams = new URLSearchParams(location.search)
-
- // 상태 관리
- const [posts, setPosts] = useState([])
- const [total, setTotal] = useState(0)
- const [skip, setSkip] = useState(parseInt(queryParams.get("skip") || "0"))
- const [limit, setLimit] = useState(parseInt(queryParams.get("limit") || "10"))
- const [searchQuery, setSearchQuery] = useState(queryParams.get("search") || "")
- const [selectedPost, setSelectedPost] = useState(null)
- const [sortBy, setSortBy] = useState(queryParams.get("sortBy") || "")
- const [sortOrder, setSortOrder] = useState(queryParams.get("sortOrder") || "asc")
- const [showAddDialog, setShowAddDialog] = useState(false)
- const [showEditDialog, setShowEditDialog] = useState(false)
- const [newPost, setNewPost] = useState({ title: "", body: "", userId: 1 })
- const [loading, setLoading] = useState(false)
- const [tags, setTags] = useState([])
- const [selectedTag, setSelectedTag] = useState(queryParams.get("tag") || "")
- const [comments, setComments] = useState({})
- const [selectedComment, setSelectedComment] = useState(null)
- const [newComment, setNewComment] = useState({ body: "", postId: null, userId: 1 })
- const [showAddCommentDialog, setShowAddCommentDialog] = useState(false)
- const [showEditCommentDialog, setShowEditCommentDialog] = useState(false)
- const [showPostDetailDialog, setShowPostDetailDialog] = useState(false)
- const [showUserModal, setShowUserModal] = useState(false)
- const [selectedUser, setSelectedUser] = useState(null)
-
- // URL 업데이트 함수
- const updateURL = () => {
- const params = new URLSearchParams()
- if (skip) params.set("skip", skip.toString())
- if (limit) params.set("limit", limit.toString())
- if (searchQuery) params.set("search", searchQuery)
- if (sortBy) params.set("sortBy", sortBy)
- if (sortOrder) params.set("sortOrder", sortOrder)
- if (selectedTag) params.set("tag", selectedTag)
- navigate(`?${params.toString()}`)
- }
-
- // 게시물 가져오기
- const fetchPosts = () => {
- setLoading(true)
- let postsData
- let usersData
-
- fetch(`/api/posts?limit=${limit}&skip=${skip}`)
- .then((response) => response.json())
- .then((data) => {
- postsData = data
- return fetch("/api/users?limit=0&select=username,image")
- })
- .then((response) => response.json())
- .then((users) => {
- usersData = users.users
- const postsWithUsers = postsData.posts.map((post) => ({
- ...post,
- author: usersData.find((user) => user.id === post.userId),
- }))
- setPosts(postsWithUsers)
- setTotal(postsData.total)
- })
- .catch((error) => {
- console.error("게시물 가져오기 오류:", error)
- })
- .finally(() => {
- setLoading(false)
- })
- }
-
- // 태그 가져오기
- const fetchTags = async () => {
- try {
- const response = await fetch("/api/posts/tags")
- const data = await response.json()
- setTags(data)
- } catch (error) {
- console.error("태그 가져오기 오류:", error)
- }
- }
-
- // 게시물 검색
- const searchPosts = async () => {
- if (!searchQuery) {
- fetchPosts()
- return
- }
- setLoading(true)
- try {
- const response = await fetch(`/api/posts/search?q=${searchQuery}`)
- const data = await response.json()
- setPosts(data.posts)
- setTotal(data.total)
- } catch (error) {
- console.error("게시물 검색 오류:", error)
- }
- setLoading(false)
- }
-
- // 태그별 게시물 가져오기
- const fetchPostsByTag = async (tag) => {
- if (!tag || tag === "all") {
- fetchPosts()
- return
- }
- setLoading(true)
- try {
- const [postsResponse, usersResponse] = await Promise.all([
- fetch(`/api/posts/tag/${tag}`),
- fetch("/api/users?limit=0&select=username,image"),
- ])
- const postsData = await postsResponse.json()
- const usersData = await usersResponse.json()
-
- const postsWithUsers = postsData.posts.map((post) => ({
- ...post,
- author: usersData.users.find((user) => user.id === post.userId),
- }))
-
- setPosts(postsWithUsers)
- setTotal(postsData.total)
- } catch (error) {
- console.error("태그별 게시물 가져오기 오류:", error)
- }
- setLoading(false)
- }
-
- // 게시물 추가
- const addPost = async () => {
- try {
- const response = await fetch("/api/posts/add", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(newPost),
- })
- const data = await response.json()
- setPosts([data, ...posts])
- setShowAddDialog(false)
- setNewPost({ title: "", body: "", userId: 1 })
- } catch (error) {
- console.error("게시물 추가 오류:", error)
- }
- }
-
- // 게시물 업데이트
- const updatePost = async () => {
- try {
- const response = await fetch(`/api/posts/${selectedPost.id}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(selectedPost),
- })
- const data = await response.json()
- setPosts(posts.map((post) => (post.id === data.id ? data : post)))
- setShowEditDialog(false)
- } catch (error) {
- console.error("게시물 업데이트 오류:", error)
- }
- }
-
- // 게시물 삭제
- const deletePost = async (id) => {
- try {
- await fetch(`/api/posts/${id}`, {
- method: "DELETE",
- })
- setPosts(posts.filter((post) => post.id !== id))
- } catch (error) {
- console.error("게시물 삭제 오류:", error)
- }
- }
-
- // 댓글 가져오기
- const fetchComments = async (postId) => {
- if (comments[postId]) return // 이미 불러온 댓글이 있으면 다시 불러오지 않음
- try {
- const response = await fetch(`/api/comments/post/${postId}`)
- const data = await response.json()
- setComments((prev) => ({ ...prev, [postId]: data.comments }))
- } catch (error) {
- console.error("댓글 가져오기 오류:", error)
- }
- }
-
- // 댓글 추가
- const addComment = async () => {
- try {
- const response = await fetch("/api/comments/add", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(newComment),
- })
- const data = await response.json()
- setComments((prev) => ({
- ...prev,
- [data.postId]: [...(prev[data.postId] || []), data],
- }))
- setShowAddCommentDialog(false)
- setNewComment({ body: "", postId: null, userId: 1 })
- } catch (error) {
- console.error("댓글 추가 오류:", error)
- }
- }
-
- // 댓글 업데이트
- const updateComment = async () => {
- try {
- const response = await fetch(`/api/comments/${selectedComment.id}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ body: selectedComment.body }),
- })
- const data = await response.json()
- setComments((prev) => ({
- ...prev,
- [data.postId]: prev[data.postId].map((comment) => (comment.id === data.id ? data : comment)),
- }))
- setShowEditCommentDialog(false)
- } catch (error) {
- console.error("댓글 업데이트 오류:", error)
- }
- }
-
- // 댓글 삭제
- const deleteComment = async (id, postId) => {
- try {
- await fetch(`/api/comments/${id}`, {
- method: "DELETE",
- })
- setComments((prev) => ({
- ...prev,
- [postId]: prev[postId].filter((comment) => comment.id !== id),
- }))
- } catch (error) {
- console.error("댓글 삭제 오류:", error)
- }
- }
-
- // 댓글 좋아요
- const likeComment = async (id, postId) => {
- try {
-
- const response = await fetch(`/api/comments/${id}`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ likes: comments[postId].find((c) => c.id === id).likes + 1 }),
- })
- const data = await response.json()
- setComments((prev) => ({
- ...prev,
- [postId]: prev[postId].map((comment) => (comment.id === data.id ? {...data, likes: comment.likes + 1} : comment)),
- }))
- } catch (error) {
- console.error("댓글 좋아요 오류:", error)
- }
- }
-
- // 게시물 상세 보기
- const openPostDetail = (post) => {
- setSelectedPost(post)
- fetchComments(post.id)
- setShowPostDetailDialog(true)
- }
-
- // 사용자 모달 열기
- const openUserModal = async (user) => {
- try {
- const response = await fetch(`/api/users/${user.id}`)
- const userData = await response.json()
- setSelectedUser(userData)
- setShowUserModal(true)
- } catch (error) {
- console.error("사용자 정보 가져오기 오류:", error)
- }
- }
-
- useEffect(() => {
- fetchTags()
- }, [])
-
- useEffect(() => {
- if (selectedTag) {
- fetchPostsByTag(selectedTag)
- } else {
- fetchPosts()
- }
- updateURL()
- }, [skip, limit, sortBy, sortOrder, selectedTag])
-
- useEffect(() => {
- const params = new URLSearchParams(location.search)
- setSkip(parseInt(params.get("skip") || "0"))
- setLimit(parseInt(params.get("limit") || "10"))
- setSearchQuery(params.get("search") || "")
- setSortBy(params.get("sortBy") || "")
- setSortOrder(params.get("sortOrder") || "asc")
- setSelectedTag(params.get("tag") || "")
- }, [location.search])
-
- // 하이라이트 함수 추가
- const highlightText = (text: string, highlight: string) => {
- if (!text) return null
- if (!highlight.trim()) {
- return {text}
- }
- const regex = new RegExp(`(${highlight})`, "gi")
- const parts = text.split(regex)
- return (
-
- {parts.map((part, i) => (regex.test(part) ? {part} : {part}))}
-
- )
- }
-
- // 게시물 테이블 렌더링
- const renderPostTable = () => (
-
-
-
- ID
- 제목
- 작성자
- 반응
- 작업
-
-
-
- {posts.map((post) => (
-
- {post.id}
-
-
-
{highlightText(post.title, searchQuery)}
-
-
- {post.tags?.map((tag) => (
- {
- setSelectedTag(tag)
- updateURL()
- }}
- >
- {tag}
-
- ))}
-
-
-
-
- openUserModal(post.author)}>
-

-
{post.author?.username}
-
-
-
-
-
- {post.reactions?.likes || 0}
-
- {post.reactions?.dislikes || 0}
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
- )
-
- // 댓글 렌더링
- const renderComments = (postId) => (
-
-
-
댓글
-
-
-
- {comments[postId]?.map((comment) => (
-
-
- {comment.user.username}:
- {highlightText(comment.body, searchQuery)}
-
-
-
-
-
-
-
- ))}
-
-
- )
-
- return (
-
-
-
- 게시물 관리자
-
-
-
-
-
- {/* 검색 및 필터 컨트롤 */}
-
-
-
-
- setSearchQuery(e.target.value)}
- onKeyPress={(e) => e.key === "Enter" && searchPosts()}
- />
-
-
-
-
-
-
-
- {/* 게시물 테이블 */}
- {loading ?
로딩 중...
: renderPostTable()}
-
- {/* 페이지네이션 */}
-
-
- 표시
-
- 항목
-
-
-
-
-
-
-
-
-
- {/* 게시물 추가 대화상자 */}
-
-
- {/* 게시물 수정 대화상자 */}
-
-
- {/* 댓글 추가 대화상자 */}
-
-
- {/* 댓글 수정 대화상자 */}
-
-
- {/* 게시물 상세 보기 대화상자 */}
-
-
- {/* 사용자 모달 */}
-
-
- )
-}
-
-export default PostsManager
diff --git a/src/pages/posts-manager-page.tsx b/src/pages/posts-manager-page.tsx
new file mode 100644
index 000000000..6a35f082e
--- /dev/null
+++ b/src/pages/posts-manager-page.tsx
@@ -0,0 +1,16 @@
+import { Card } from "@/shared/ui";
+import { PostsBodyWidget } from "@/widgets/posts-manager/ui/posts-body-widget";
+import { PostsDialogsWidget } from "@/widgets/posts-manager/ui/posts-dialogs-widget";
+import { PostsHeaderWidget } from "@/widgets/posts-manager/ui/posts-header-widget";
+
+const PostsManager = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default PostsManager;
diff --git a/src/shared/api/client.ts b/src/shared/api/client.ts
new file mode 100644
index 000000000..19721d2ac
--- /dev/null
+++ b/src/shared/api/client.ts
@@ -0,0 +1,80 @@
+import { BASE_URL } from "@/shared/lib/env";
+
+type Query = Record;
+
+function toAbsoluteBase(base: string): string {
+ if (/^https?:\/\//.test(base)) return base;
+ const origin =
+ typeof window !== "undefined" && window.location && window.location.origin
+ ? window.location.origin
+ : "http://localhost";
+ const normalized = base.startsWith("/") ? base : `/${base}`;
+ return origin + normalized;
+}
+
+function buildUrl(base: string, path: string, params?: Query) {
+ const absoluteBase = toAbsoluteBase(base);
+ const url = new URL(path.replace(/^\//, ""), absoluteBase.endsWith("/") ? absoluteBase : absoluteBase + "/");
+ if (params) {
+ Object.entries(params).forEach(([key, value]) => {
+ if (value === undefined) return;
+ const serialized = Array.isArray(value)
+ ? value.map(String).join(",")
+ : typeof value === "string"
+ ? value.replace(/\s*,\s*/g, ",").trim()
+ : String(value);
+ url.searchParams.set(key, serialized);
+ });
+ }
+ return url.toString();
+}
+
+export interface HttpClient {
+ get(path: string, options?: { params?: Query; headers?: HeadersInit }): Promise;
+ post(path: string, body?: unknown, options?: { headers?: HeadersInit }): Promise;
+ put(path: string, body?: unknown, options?: { headers?: HeadersInit }): Promise;
+ patch(path: string, body?: unknown, options?: { headers?: HeadersInit }): Promise;
+ delete(path: string, options?: { headers?: HeadersInit }): Promise;
+}
+
+export function createHttpClient(baseUrl: string): HttpClient {
+ async function request(input: RequestInfo, init?: RequestInit): Promise {
+ const res = await fetch(input, init);
+ if (!res.ok) {
+ const text = await res.text().catch(() => "");
+ throw new Error(`HTTP ${res.status} ${res.statusText} ${text ? `- ${text}` : ""}`);
+ }
+ return (await res.json()) as T;
+ }
+
+ return {
+ get: (path, options) =>
+ request(buildUrl(baseUrl, path, options?.params), {
+ method: "GET",
+ headers: options?.headers,
+ // Avoid 304 Not Modified returning empty body in browsers
+ cache: "no-store",
+ }),
+ post: (path, body, options) =>
+ request(buildUrl(baseUrl, path), {
+ method: "POST",
+ headers: { "Content-Type": "application/json", ...options?.headers },
+ body: body == null ? undefined : JSON.stringify(body),
+ }),
+ put: (path, body, options) =>
+ request(buildUrl(baseUrl, path), {
+ method: "PUT",
+ headers: { "Content-Type": "application/json", ...options?.headers },
+ body: body == null ? undefined : JSON.stringify(body),
+ }),
+ patch: (path, body, options) =>
+ request(buildUrl(baseUrl, path), {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json", ...options?.headers },
+ body: body == null ? undefined : JSON.stringify(body),
+ }),
+ delete: (path, options) => request(buildUrl(baseUrl, path), { method: "DELETE", headers: options?.headers }),
+ };
+}
+
+export const http = createHttpClient(BASE_URL);
diff --git a/src/shared/lib/env.ts b/src/shared/lib/env.ts
new file mode 100644
index 000000000..9d208c20e
--- /dev/null
+++ b/src/shared/lib/env.ts
@@ -0,0 +1,23 @@
+export type EnvRecord = Record;
+
+export const getViteEnv = (): EnvRecord => {
+ try {
+ const meta = import.meta as { env?: EnvRecord };
+ return meta.env ?? {};
+ } catch {
+ return {};
+ }
+};
+
+const env = getViteEnv();
+
+export const ENV_USE_SERVER_TRUTH = String(env.VITE_USE_SERVER_TRUTH || "false") === "true";
+
+const isAbsoluteHttp = (value?: string): boolean => !!value && /^https?:\/\//.test(value);
+const mode = (import.meta as unknown as { env?: EnvRecord }).env?.MODE;
+
+export const BASE_URL = isAbsoluteHttp(env.VITE_API_BASE_URL)
+ ? (env.VITE_API_BASE_URL as string)
+ : mode === "development"
+ ? env.VITE_API_BASE_URL || "/api"
+ : "https://dummyjson.com";
diff --git a/src/shared/lib/split-by-highlight.ts b/src/shared/lib/split-by-highlight.ts
new file mode 100644
index 000000000..e5b73c213
--- /dev/null
+++ b/src/shared/lib/split-by-highlight.ts
@@ -0,0 +1,13 @@
+export interface HighlightSegment {
+ value: string;
+ highlighted: boolean;
+}
+
+export const splitByHighlight = (text: string, highlight: string): HighlightSegment[] => {
+ if (!text) return [];
+ if (!highlight.trim()) return [{ value: text, highlighted: false }];
+ const regex = new RegExp(`(${highlight})`, "gi");
+ const parts = text.split(regex);
+
+ return parts.map((v, i) => ({ value: v, highlighted: i % 2 === 1 }));
+};
diff --git a/src/shared/ui/button.tsx b/src/shared/ui/button.tsx
new file mode 100644
index 000000000..4685c269f
--- /dev/null
+++ b/src/shared/ui/button.tsx
@@ -0,0 +1,38 @@
+import { cva, VariantProps } from "class-variance-authority";
+import { ButtonHTMLAttributes, forwardRef } from "react";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
+ {
+ variants: {
+ variant: {
+ default: "bg-blue-500 text-white hover:bg-blue-600",
+ destructive: "bg-red-500 text-white hover:bg-red-600",
+ outline: "border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-100",
+ secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
+ ghost: "bg-transparent text-gray-700 hover:bg-gray-100",
+ link: "underline-offset-4 hover:underline text-blue-500",
+ },
+ size: {
+ default: "h-10 py-2 px-4",
+ sm: "h-8 px-3 rounded-md text-xs",
+ lg: "h-11 px-8 rounded-md",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+interface ButtonProps extends ButtonHTMLAttributes, VariantProps {
+ className?: string;
+}
+
+export const Button = forwardRef(({ className, variant, size, ...props }, ref) => {
+ return ;
+});
+
+Button.displayName = "Button";
diff --git a/src/shared/ui/card.tsx b/src/shared/ui/card.tsx
new file mode 100644
index 000000000..3ddb1dd3f
--- /dev/null
+++ b/src/shared/ui/card.tsx
@@ -0,0 +1,41 @@
+import { forwardRef, HTMLAttributes } from "react";
+
+interface CardProps extends HTMLAttributes {
+ className?: string;
+}
+
+export const Card = forwardRef(({ className, ...props }, ref) => (
+
+));
+
+Card.displayName = "Card";
+
+interface CardHeaderProps extends HTMLAttributes {
+ className?: string;
+}
+
+export const CardHeader = forwardRef(({ className, ...props }, ref) => (
+
+));
+
+CardHeader.displayName = "CardHeader";
+
+interface CardTitleProps extends React.HTMLAttributes {
+ className?: string;
+}
+
+export const CardTitle = forwardRef(({ className, ...props }, ref) => (
+
+));
+
+CardTitle.displayName = "CardTitle";
+
+interface CardContentProps extends React.HTMLAttributes {
+ className?: string;
+}
+
+export const CardContent = forwardRef(({ className, ...props }, ref) => (
+
+));
+
+CardContent.displayName = "CardContent";
diff --git a/src/shared/ui/dialog.tsx b/src/shared/ui/dialog.tsx
new file mode 100644
index 000000000..c2ddeeb7b
--- /dev/null
+++ b/src/shared/ui/dialog.tsx
@@ -0,0 +1,59 @@
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { X } from "lucide-react";
+import { ComponentPropsWithoutRef, ComponentRef, forwardRef, HTMLAttributes } from "react";
+
+export const Dialog = DialogPrimitive.Root;
+export const DialogTrigger = DialogPrimitive.Trigger;
+export const DialogPortal = DialogPrimitive.Portal;
+export const DialogOverlay = DialogPrimitive.Overlay;
+
+interface DialogContentProps extends ComponentPropsWithoutRef {
+ className?: string;
+}
+
+export const DialogContent = forwardRef, DialogContentProps>(
+ ({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ 닫기
+
+
+
+ ),
+);
+
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+interface DialogHeaderProps extends HTMLAttributes {
+ className?: string;
+}
+
+export const DialogHeader = ({ className, ...props }: DialogHeaderProps) => (
+
+);
+
+DialogHeader.displayName = "DialogHeader";
+
+interface DialogTitleProps extends ComponentPropsWithoutRef {
+ className?: string;
+}
+
+export const DialogTitle = forwardRef, DialogTitleProps>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
diff --git a/src/shared/ui/highlight-text.tsx b/src/shared/ui/highlight-text.tsx
new file mode 100644
index 000000000..24fd4106a
--- /dev/null
+++ b/src/shared/ui/highlight-text.tsx
@@ -0,0 +1,13 @@
+import type { HighlightSegment } from "@shared/lib/split-by-highlight";
+
+type HighlightTextProps = {
+ segments: HighlightSegment[];
+};
+
+export const HighlightText = ({ segments }: HighlightTextProps) => {
+ return (
+
+ {segments.map((s, i) => (s.highlighted ? {s.value} : {s.value}))}
+
+ );
+};
diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts
new file mode 100644
index 000000000..6b680d66c
--- /dev/null
+++ b/src/shared/ui/index.ts
@@ -0,0 +1,7 @@
+export { Button } from "@/shared/ui/button";
+export { Input } from "@/shared/ui/input";
+export { Card, CardHeader, CardTitle, CardContent } from "./card";
+export { Textarea } from "./textarea";
+export { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
+export { Dialog, DialogContent, DialogHeader, DialogTitle } from "./dialog";
+export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table";
diff --git a/src/shared/ui/input.tsx b/src/shared/ui/input.tsx
new file mode 100644
index 000000000..107396152
--- /dev/null
+++ b/src/shared/ui/input.tsx
@@ -0,0 +1,18 @@
+import { forwardRef, InputHTMLAttributes } from "react";
+
+interface InputProps extends InputHTMLAttributes {
+ className?: string;
+}
+
+export const Input = forwardRef(({ className, type, ...props }, ref) => {
+ return (
+
+ );
+});
+
+Input.displayName = "Input";
diff --git a/src/shared/ui/select.tsx b/src/shared/ui/select.tsx
new file mode 100644
index 000000000..a504808e8
--- /dev/null
+++ b/src/shared/ui/select.tsx
@@ -0,0 +1,70 @@
+import * as SelectPrimitive from "@radix-ui/react-select";
+import { Check, ChevronDown } from "lucide-react";
+import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react";
+
+export const Select = SelectPrimitive.Root;
+export const SelectGroup = SelectPrimitive.Group;
+export const SelectValue = SelectPrimitive.Value;
+
+interface SelectTriggerProps extends ComponentPropsWithoutRef {
+ className?: string;
+}
+
+export const SelectTrigger = forwardRef, SelectTriggerProps>(
+ ({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+ ),
+);
+
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
+
+interface SelectContentProps extends ComponentPropsWithoutRef {
+ className?: string;
+}
+
+export const SelectContent = forwardRef, SelectContentProps>(
+ ({ className, children, position = "popper", ...props }, ref) => (
+
+
+ {children}
+
+
+ ),
+);
+
+SelectContent.displayName = SelectPrimitive.Content.displayName;
+
+interface SelectItemProps extends ComponentPropsWithoutRef {
+ className?: string;
+}
+
+export const SelectItem = forwardRef, SelectItemProps>(
+ ({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+ ),
+);
+
+SelectItem.displayName = SelectPrimitive.Item.displayName;
diff --git a/src/shared/ui/table.tsx b/src/shared/ui/table.tsx
new file mode 100644
index 000000000..6c7277ac0
--- /dev/null
+++ b/src/shared/ui/table.tsx
@@ -0,0 +1,71 @@
+import { forwardRef, HTMLAttributes, TableHTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "react";
+
+interface TableProps extends TableHTMLAttributes {
+ className?: string;
+}
+
+export const Table = forwardRef(({ className, ...props }, ref) => (
+
+));
+
+Table.displayName = "Table";
+
+interface TableHeaderProps extends HTMLAttributes {
+ className?: string;
+}
+
+export const TableHeader = forwardRef(({ className, ...props }, ref) => (
+
+));
+
+TableHeader.displayName = "TableHeader";
+
+interface TableBodyProps extends HTMLAttributes {
+ className?: string;
+}
+
+export const TableBody = forwardRef(({ className, ...props }, ref) => (
+
+));
+
+TableBody.displayName = "TableBody";
+
+interface TableRowProps extends HTMLAttributes {
+ className?: string;
+}
+
+export const TableRow = forwardRef(({ className, ...props }, ref) => (
+
+));
+
+TableRow.displayName = "TableRow";
+
+interface TableHeadProps extends ThHTMLAttributes {
+ className?: string;
+}
+
+export const TableHead = forwardRef(({ className, ...props }, ref) => (
+ |
+));
+
+TableHead.displayName = "TableHead";
+
+interface TableCellProps extends TdHTMLAttributes {
+ className?: string;
+}
+
+export const TableCell = forwardRef(({ className, ...props }, ref) => (
+ |
+));
+
+TableCell.displayName = "TableCell";
diff --git a/src/shared/ui/textarea.tsx b/src/shared/ui/textarea.tsx
new file mode 100644
index 000000000..a12609a1d
--- /dev/null
+++ b/src/shared/ui/textarea.tsx
@@ -0,0 +1,17 @@
+import { forwardRef, TextareaHTMLAttributes } from "react";
+
+interface TextareaProps extends TextareaHTMLAttributes {
+ className?: string;
+}
+
+export const Textarea = forwardRef(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+
+Textarea.displayName = "Textarea";
diff --git a/src/types/style.d.ts b/src/types/style.d.ts
new file mode 100644
index 000000000..cbe652dbe
--- /dev/null
+++ b/src/types/style.d.ts
@@ -0,0 +1 @@
+declare module "*.css";
diff --git a/src/widgets/footer/ui/footer.tsx b/src/widgets/footer/ui/footer.tsx
new file mode 100644
index 000000000..eaafbff8f
--- /dev/null
+++ b/src/widgets/footer/ui/footer.tsx
@@ -0,0 +1,9 @@
+export function Footer() {
+ return (
+
+ );
+}
diff --git a/src/widgets/header/ui/header.tsx b/src/widgets/header/ui/header.tsx
new file mode 100644
index 000000000..b1ff1f44b
--- /dev/null
+++ b/src/widgets/header/ui/header.tsx
@@ -0,0 +1,33 @@
+import { MessageSquare } from "lucide-react";
+
+export function Header() {
+ return (
+
+
+
+
+
게시물 관리 시스템
+
+
+
+
+ );
+}
diff --git a/src/widgets/posts-manager/ui/posts-body-widget.tsx b/src/widgets/posts-manager/ui/posts-body-widget.tsx
new file mode 100644
index 000000000..236a0e6bb
--- /dev/null
+++ b/src/widgets/posts-manager/ui/posts-body-widget.tsx
@@ -0,0 +1,36 @@
+import { CardContent } from "@shared/ui";
+
+import { SortOrder, usePostSearchParams } from "@/features/post-filter";
+import { PostFilterContainer } from "@/features/post-filter/ui/post-filter-container";
+import { PostsTableContainer } from "@/features/post-load";
+import { usePostsQuery } from "@/features/post-load/model/posts.query.ts";
+import { PostPagination } from "@/features/post-pagination/ui/post-pagination";
+
+export function PostsBodyWidget() {
+ const { params, setParams } = usePostSearchParams();
+ const { data, isFetching } = usePostsQuery({
+ limit: params.limit,
+ skip: params.skip,
+ tag: params.tag,
+ search: params.search,
+ sortBy: params.sortBy,
+ sortOrder: params.sortOrder as SortOrder,
+ });
+
+ return (
+
+
+
+ {isFetching ?
로딩 중...
:
}
+
setParams({ limit: Number(value), skip: 0 })}
+ onPrev={() => setParams({ skip: Math.max(0, params.skip - params.limit) })}
+ onNext={() => setParams({ skip: params.skip + params.limit })}
+ />
+
+
+ );
+}
diff --git a/src/widgets/posts-manager/ui/posts-dialogs-widget.tsx b/src/widgets/posts-manager/ui/posts-dialogs-widget.tsx
new file mode 100644
index 000000000..788e228c0
--- /dev/null
+++ b/src/widgets/posts-manager/ui/posts-dialogs-widget.tsx
@@ -0,0 +1,28 @@
+import { PostDetailDialog, usePosts } from "@/entities/post";
+import { CommentAddDialogContainer, CommentEditDialogContainer } from "@/features/comment-edit";
+import { PostAddDialogContainer, PostEditDialogContainer } from "@/features/post-edit";
+import { usePostSearchParams } from "@/features/post-filter/model/filter-post.hook";
+import { UserDetailDialogContainer } from "@/features/user-load";
+
+export function PostsDialogsWidget() {
+ const { selectedPost, isDetailOpen, setIsDetailOpen } = usePosts();
+ const { params } = usePostSearchParams();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/widgets/posts-manager/ui/posts-header-widget.tsx b/src/widgets/posts-manager/ui/posts-header-widget.tsx
new file mode 100644
index 000000000..ad4575535
--- /dev/null
+++ b/src/widgets/posts-manager/ui/posts-header-widget.tsx
@@ -0,0 +1,22 @@
+import { Plus } from "lucide-react";
+
+import { Button, CardHeader, CardTitle } from "@shared/ui";
+
+import { useNewPostForm } from "@/features/post-edit";
+
+export function PostsHeaderWidget() {
+ const { setIsAddOpen: setIsAddPostOpen } = useNewPostForm();
+
+ return (
+
+
+ 게시물 관리자
+
+
+
+ );
+}
+
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 5a2def4b7..b95cce589 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -5,6 +5,17 @@
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
+ "baseUrl": "src",
+ "paths": {
+ "@/*": ["*"],
+ "@app/*": ["app/*"],
+ "@pages/*": ["pages/*"],
+ "@widgets/*": ["widgets/*"],
+ "@features/*": ["features/*"],
+ "@entities/*": ["entities/*"],
+ "@shared/*": ["shared/*"],
+ "@components/*": ["components/*"]
+ },
/* Bundler mode */
"moduleResolution": "Bundler",
diff --git a/vite.config.ts b/vite.config.ts
index be7b7a3d4..fa3e8dd68 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,9 +1,24 @@
-import { defineConfig } from "vite"
-import react from "@vitejs/plugin-react"
+import path from "node:path";
+
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
+ base: "/front_6th_chapter2-3/",
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "src"),
+ "@app": path.resolve(__dirname, "src/app"),
+ "@pages": path.resolve(__dirname, "src/pages"),
+ "@widgets": path.resolve(__dirname, "src/widgets"),
+ "@features": path.resolve(__dirname, "src/features"),
+ "@entities": path.resolve(__dirname, "src/entities"),
+ "@shared": path.resolve(__dirname, "src/shared"),
+ "@components": path.resolve(__dirname, "src/components"),
+ },
+ },
server: {
proxy: {
"/api": {
@@ -14,4 +29,4 @@ export default defineConfig({
},
},
},
-})
+});