diff --git a/.pnp.cjs b/.pnp.cjs
index 7b74fcd..ca2f71a 100755
--- a/.pnp.cjs
+++ b/.pnp.cjs
@@ -1960,6 +1960,43 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["@gilbarbara/deep-equal", [\
+ ["npm:0.1.2", {\
+ "packageLocation": "./.yarn/cache/@gilbarbara-deep-equal-npm-0.1.2-98a7c259d1-ef441034a3.zip/node_modules/@gilbarbara/deep-equal/",\
+ "packageDependencies": [\
+ ["@gilbarbara/deep-equal", "npm:0.1.2"]\
+ ],\
+ "linkType": "HARD"\
+ }],\
+ ["npm:0.3.1", {\
+ "packageLocation": "./.yarn/cache/@gilbarbara-deep-equal-npm-0.3.1-59e6185977-009584aa91.zip/node_modules/@gilbarbara/deep-equal/",\
+ "packageDependencies": [\
+ ["@gilbarbara/deep-equal", "npm:0.3.1"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
+ ["@gilbarbara/helpers", [\
+ ["npm:0.9.0", {\
+ "packageLocation": "./.yarn/cache/@gilbarbara-helpers-npm-0.9.0-3aabeaa261-07457da54e.zip/node_modules/@gilbarbara/helpers/",\
+ "packageDependencies": [\
+ ["@gilbarbara/helpers", "npm:0.9.0"],\
+ ["@gilbarbara/types", "npm:0.2.2"],\
+ ["is-lite", "npm:1.2.0"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
+ ["@gilbarbara/types", [\
+ ["npm:0.2.2", {\
+ "packageLocation": "./.yarn/cache/@gilbarbara-types-npm-0.2.2-40872223ec-c998bfb41f.zip/node_modules/@gilbarbara/types/",\
+ "packageDependencies": [\
+ ["@gilbarbara/types", "npm:0.2.2"],\
+ ["type-fest", "npm:4.8.3"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["@humanwhocodes/config-array", [\
["npm:0.11.13", {\
"packageLocation": "./.yarn/cache/@humanwhocodes-config-array-npm-0.11.13-12314014f2-d76ca802d8.zip/node_modules/@humanwhocodes/config-array/",\
@@ -7100,6 +7137,7 @@ const RAW_RUNTIME_STATE =
["react-dom", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:18.2.0"],\
["react-full-screen", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:1.1.1"],\
["react-howler", "npm:5.2.0"],\
+ ["react-joyride", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:2.7.1"],\
["react-markdown", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:9.0.1"],\
["react-router-dom", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:6.18.0"],\
["remark-gfm", "npm:4.0.0"],\
@@ -7693,6 +7731,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["deep-diff", [\
+ ["npm:1.0.2", {\
+ "packageLocation": "./.yarn/cache/deep-diff-npm-1.0.2-ab33725091-cc3e315ba9.zip/node_modules/deep-diff/",\
+ "packageDependencies": [\
+ ["deep-diff", "npm:1.0.2"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["deep-extend", [\
["npm:0.6.0", {\
"packageLocation": "./.yarn/cache/deep-extend-npm-0.6.0-e182924219-1c6b0abcdb.zip/node_modules/deep-extend/",\
@@ -10032,6 +10079,22 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["is-lite", [\
+ ["npm:0.8.2", {\
+ "packageLocation": "./.yarn/cache/is-lite-npm-0.8.2-e09c91529d-ed4b99d47f.zip/node_modules/is-lite/",\
+ "packageDependencies": [\
+ ["is-lite", "npm:0.8.2"]\
+ ],\
+ "linkType": "HARD"\
+ }],\
+ ["npm:1.2.0", {\
+ "packageLocation": "./.yarn/cache/is-lite-npm-1.2.0-304a5ffe73-3bde0175c5.zip/node_modules/is-lite/",\
+ "packageDependencies": [\
+ ["is-lite", "npm:1.2.0"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["is-number", [\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/is-number-npm-7.0.0-060086935c-b4686d0d30.zip/node_modules/is-number/",\
@@ -13229,6 +13292,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["popper.js", [\
+ ["npm:1.16.1", {\
+ "packageLocation": "./.yarn/cache/popper.js-npm-1.16.1-a99192bd83-1c1a826f75.zip/node_modules/popper.js/",\
+ "packageDependencies": [\
+ ["popper.js", "npm:1.16.1"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["postcss", [\
["npm:8.4.31", {\
"packageLocation": "./.yarn/cache/postcss-npm-8.4.31-385051a82b-748b82e6e5.zip/node_modules/postcss/",\
@@ -13748,6 +13820,37 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["react-floater", [\
+ ["npm:0.7.9", {\
+ "packageLocation": "./.yarn/cache/react-floater-npm-0.7.9-c1546b4d7f-3fa58e968a.zip/node_modules/react-floater/",\
+ "packageDependencies": [\
+ ["react-floater", "npm:0.7.9"]\
+ ],\
+ "linkType": "SOFT"\
+ }],\
+ ["virtual:4485e824601cbf62dbb543329e010c4c57901e81da38004404d78e06c37008edf008fd7a69426559e914bf4c58ed75c89658d47a0d07c7e06339ffd4b9695cfe#npm:0.7.9", {\
+ "packageLocation": "./.yarn/__virtual__/react-floater-virtual-f53c313b67/0/cache/react-floater-npm-0.7.9-c1546b4d7f-3fa58e968a.zip/node_modules/react-floater/",\
+ "packageDependencies": [\
+ ["react-floater", "virtual:4485e824601cbf62dbb543329e010c4c57901e81da38004404d78e06c37008edf008fd7a69426559e914bf4c58ed75c89658d47a0d07c7e06339ffd4b9695cfe#npm:0.7.9"],\
+ ["@types/react", "npm:18.2.37"],\
+ ["@types/react-dom", "npm:18.2.15"],\
+ ["deepmerge", "npm:4.3.1"],\
+ ["is-lite", "npm:0.8.2"],\
+ ["popper.js", "npm:1.16.1"],\
+ ["prop-types", "npm:15.8.1"],\
+ ["react", "npm:18.2.0"],\
+ ["react-dom", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:18.2.0"],\
+ ["tree-changes", "npm:0.9.3"]\
+ ],\
+ "packagePeers": [\
+ "@types/react-dom",\
+ "@types/react",\
+ "react-dom",\
+ "react"\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["react-full-screen", [\
["npm:1.1.1", {\
"packageLocation": "./.yarn/cache/react-full-screen-npm-1.1.1-4d2e312e1b-02c6034c6a.zip/node_modules/react-full-screen/",\
@@ -13782,6 +13885,28 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["react-innertext", [\
+ ["npm:1.1.5", {\
+ "packageLocation": "./.yarn/cache/react-innertext-npm-1.1.5-50613b71c6-45335918ac.zip/node_modules/react-innertext/",\
+ "packageDependencies": [\
+ ["react-innertext", "npm:1.1.5"]\
+ ],\
+ "linkType": "SOFT"\
+ }],\
+ ["virtual:4485e824601cbf62dbb543329e010c4c57901e81da38004404d78e06c37008edf008fd7a69426559e914bf4c58ed75c89658d47a0d07c7e06339ffd4b9695cfe#npm:1.1.5", {\
+ "packageLocation": "./.yarn/__virtual__/react-innertext-virtual-e856265ae0/0/cache/react-innertext-npm-1.1.5-50613b71c6-45335918ac.zip/node_modules/react-innertext/",\
+ "packageDependencies": [\
+ ["react-innertext", "virtual:4485e824601cbf62dbb543329e010c4c57901e81da38004404d78e06c37008edf008fd7a69426559e914bf4c58ed75c89658d47a0d07c7e06339ffd4b9695cfe#npm:1.1.5"],\
+ ["@types/react", "npm:18.2.37"],\
+ ["react", "npm:18.2.0"]\
+ ],\
+ "packagePeers": [\
+ "@types/react",\
+ "react"\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["react-is", [\
["npm:16.13.1", {\
"packageLocation": "./.yarn/cache/react-is-npm-16.13.1-a9b9382b4f-33977da7a5.zip/node_modules/react-is/",\
@@ -13798,6 +13923,44 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["react-joyride", [\
+ ["npm:2.7.1", {\
+ "packageLocation": "./.yarn/cache/react-joyride-npm-2.7.1-78984ee8a0-4aefbe8a6a.zip/node_modules/react-joyride/",\
+ "packageDependencies": [\
+ ["react-joyride", "npm:2.7.1"]\
+ ],\
+ "linkType": "SOFT"\
+ }],\
+ ["virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:2.7.1", {\
+ "packageLocation": "./.yarn/__virtual__/react-joyride-virtual-4485e82460/0/cache/react-joyride-npm-2.7.1-78984ee8a0-4aefbe8a6a.zip/node_modules/react-joyride/",\
+ "packageDependencies": [\
+ ["react-joyride", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:2.7.1"],\
+ ["@gilbarbara/deep-equal", "npm:0.3.1"],\
+ ["@gilbarbara/helpers", "npm:0.9.0"],\
+ ["@types/react", "npm:18.2.37"],\
+ ["@types/react-dom", "npm:18.2.15"],\
+ ["deep-diff", "npm:1.0.2"],\
+ ["deepmerge", "npm:4.3.1"],\
+ ["is-lite", "npm:1.2.0"],\
+ ["react", "npm:18.2.0"],\
+ ["react-dom", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:18.2.0"],\
+ ["react-floater", "virtual:4485e824601cbf62dbb543329e010c4c57901e81da38004404d78e06c37008edf008fd7a69426559e914bf4c58ed75c89658d47a0d07c7e06339ffd4b9695cfe#npm:0.7.9"],\
+ ["react-innertext", "virtual:4485e824601cbf62dbb543329e010c4c57901e81da38004404d78e06c37008edf008fd7a69426559e914bf4c58ed75c89658d47a0d07c7e06339ffd4b9695cfe#npm:1.1.5"],\
+ ["react-is", "npm:16.13.1"],\
+ ["scroll", "npm:3.0.1"],\
+ ["scrollparent", "npm:2.1.0"],\
+ ["tree-changes", "npm:0.11.2"],\
+ ["type-fest", "npm:4.8.3"]\
+ ],\
+ "packagePeers": [\
+ "@types/react-dom",\
+ "@types/react",\
+ "react-dom",\
+ "react"\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["react-markdown", [\
["npm:9.0.1", {\
"packageLocation": "./.yarn/cache/react-markdown-npm-9.0.1-02c77a4123-3a3895dbd5.zip/node_modules/react-markdown/",\
@@ -14437,6 +14600,24 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["scroll", [\
+ ["npm:3.0.1", {\
+ "packageLocation": "./.yarn/cache/scroll-npm-3.0.1-26a443f234-5b0c98089b.zip/node_modules/scroll/",\
+ "packageDependencies": [\
+ ["scroll", "npm:3.0.1"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
+ ["scrollparent", [\
+ ["npm:2.1.0", {\
+ "packageLocation": "./.yarn/cache/scrollparent-npm-2.1.0-2ce4ab214f-b377ec5a46.zip/node_modules/scrollparent/",\
+ "packageDependencies": [\
+ ["scrollparent", "npm:2.1.0"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["semver", [\
["npm:6.3.1", {\
"packageLocation": "./.yarn/cache/semver-npm-6.3.1-bcba31fdbe-e3d79b6090.zip/node_modules/semver/",\
@@ -15553,6 +15734,26 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["tree-changes", [\
+ ["npm:0.11.2", {\
+ "packageLocation": "./.yarn/cache/tree-changes-npm-0.11.2-eb720d8e1c-bcdcf9b550.zip/node_modules/tree-changes/",\
+ "packageDependencies": [\
+ ["tree-changes", "npm:0.11.2"],\
+ ["@gilbarbara/deep-equal", "npm:0.3.1"],\
+ ["is-lite", "npm:1.2.0"]\
+ ],\
+ "linkType": "HARD"\
+ }],\
+ ["npm:0.9.3", {\
+ "packageLocation": "./.yarn/cache/tree-changes-npm-0.9.3-e337138b32-e7a38ed3c2.zip/node_modules/tree-changes/",\
+ "packageDependencies": [\
+ ["tree-changes", "npm:0.9.3"],\
+ ["@gilbarbara/deep-equal", "npm:0.1.2"],\
+ ["is-lite", "npm:0.8.2"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["tree-kill", [\
["npm:1.2.2", {\
"packageLocation": "./.yarn/cache/tree-kill-npm-1.2.2-3da0e5a759-7b1b7c7f17.zip/node_modules/tree-kill/",\
@@ -15923,6 +16124,13 @@ const RAW_RUNTIME_STATE =
["type-fest", "npm:0.21.3"]\
],\
"linkType": "HARD"\
+ }],\
+ ["npm:4.8.3", {\
+ "packageLocation": "./.yarn/cache/type-fest-npm-4.8.3-5f50002256-978edf5d00.zip/node_modules/type-fest/",\
+ "packageDependencies": [\
+ ["type-fest", "npm:4.8.3"]\
+ ],\
+ "linkType": "HARD"\
}]\
]],\
["type-is", [\
diff --git a/.yarn/cache/@gilbarbara-deep-equal-npm-0.1.2-98a7c259d1-ef441034a3.zip b/.yarn/cache/@gilbarbara-deep-equal-npm-0.1.2-98a7c259d1-ef441034a3.zip
new file mode 100644
index 0000000..15fc5c2
Binary files /dev/null and b/.yarn/cache/@gilbarbara-deep-equal-npm-0.1.2-98a7c259d1-ef441034a3.zip differ
diff --git a/.yarn/cache/@gilbarbara-deep-equal-npm-0.3.1-59e6185977-009584aa91.zip b/.yarn/cache/@gilbarbara-deep-equal-npm-0.3.1-59e6185977-009584aa91.zip
new file mode 100644
index 0000000..a201ca0
Binary files /dev/null and b/.yarn/cache/@gilbarbara-deep-equal-npm-0.3.1-59e6185977-009584aa91.zip differ
diff --git a/.yarn/cache/@gilbarbara-helpers-npm-0.9.0-3aabeaa261-07457da54e.zip b/.yarn/cache/@gilbarbara-helpers-npm-0.9.0-3aabeaa261-07457da54e.zip
new file mode 100644
index 0000000..75e9a97
Binary files /dev/null and b/.yarn/cache/@gilbarbara-helpers-npm-0.9.0-3aabeaa261-07457da54e.zip differ
diff --git a/.yarn/cache/@gilbarbara-types-npm-0.2.2-40872223ec-c998bfb41f.zip b/.yarn/cache/@gilbarbara-types-npm-0.2.2-40872223ec-c998bfb41f.zip
new file mode 100644
index 0000000..24ad786
Binary files /dev/null and b/.yarn/cache/@gilbarbara-types-npm-0.2.2-40872223ec-c998bfb41f.zip differ
diff --git a/.yarn/cache/deep-diff-npm-1.0.2-ab33725091-cc3e315ba9.zip b/.yarn/cache/deep-diff-npm-1.0.2-ab33725091-cc3e315ba9.zip
new file mode 100644
index 0000000..9e09547
Binary files /dev/null and b/.yarn/cache/deep-diff-npm-1.0.2-ab33725091-cc3e315ba9.zip differ
diff --git a/.yarn/cache/is-lite-npm-0.8.2-e09c91529d-ed4b99d47f.zip b/.yarn/cache/is-lite-npm-0.8.2-e09c91529d-ed4b99d47f.zip
new file mode 100644
index 0000000..7b08d5b
Binary files /dev/null and b/.yarn/cache/is-lite-npm-0.8.2-e09c91529d-ed4b99d47f.zip differ
diff --git a/.yarn/cache/is-lite-npm-1.2.0-304a5ffe73-3bde0175c5.zip b/.yarn/cache/is-lite-npm-1.2.0-304a5ffe73-3bde0175c5.zip
new file mode 100644
index 0000000..fb5a7dc
Binary files /dev/null and b/.yarn/cache/is-lite-npm-1.2.0-304a5ffe73-3bde0175c5.zip differ
diff --git a/.yarn/cache/leva-npm-0.9.35-da054c7dd9-9555db5e0b.zip b/.yarn/cache/leva-npm-0.9.35-da054c7dd9-9555db5e0b.zip
index 68dab39..b550a17 100644
Binary files a/.yarn/cache/leva-npm-0.9.35-da054c7dd9-9555db5e0b.zip and b/.yarn/cache/leva-npm-0.9.35-da054c7dd9-9555db5e0b.zip differ
diff --git a/.yarn/cache/popper.js-npm-1.16.1-a99192bd83-1c1a826f75.zip b/.yarn/cache/popper.js-npm-1.16.1-a99192bd83-1c1a826f75.zip
new file mode 100644
index 0000000..1a4a1a2
Binary files /dev/null and b/.yarn/cache/popper.js-npm-1.16.1-a99192bd83-1c1a826f75.zip differ
diff --git a/.yarn/cache/react-floater-npm-0.7.9-c1546b4d7f-3fa58e968a.zip b/.yarn/cache/react-floater-npm-0.7.9-c1546b4d7f-3fa58e968a.zip
new file mode 100644
index 0000000..764a313
Binary files /dev/null and b/.yarn/cache/react-floater-npm-0.7.9-c1546b4d7f-3fa58e968a.zip differ
diff --git a/.yarn/cache/react-innertext-npm-1.1.5-50613b71c6-45335918ac.zip b/.yarn/cache/react-innertext-npm-1.1.5-50613b71c6-45335918ac.zip
new file mode 100644
index 0000000..69b8566
Binary files /dev/null and b/.yarn/cache/react-innertext-npm-1.1.5-50613b71c6-45335918ac.zip differ
diff --git a/.yarn/cache/react-joyride-npm-2.7.1-78984ee8a0-4aefbe8a6a.zip b/.yarn/cache/react-joyride-npm-2.7.1-78984ee8a0-4aefbe8a6a.zip
new file mode 100644
index 0000000..54df1e4
Binary files /dev/null and b/.yarn/cache/react-joyride-npm-2.7.1-78984ee8a0-4aefbe8a6a.zip differ
diff --git a/.yarn/cache/scroll-npm-3.0.1-26a443f234-5b0c98089b.zip b/.yarn/cache/scroll-npm-3.0.1-26a443f234-5b0c98089b.zip
new file mode 100644
index 0000000..7958b8d
Binary files /dev/null and b/.yarn/cache/scroll-npm-3.0.1-26a443f234-5b0c98089b.zip differ
diff --git a/.yarn/cache/scrollparent-npm-2.1.0-2ce4ab214f-b377ec5a46.zip b/.yarn/cache/scrollparent-npm-2.1.0-2ce4ab214f-b377ec5a46.zip
new file mode 100644
index 0000000..8077a2d
Binary files /dev/null and b/.yarn/cache/scrollparent-npm-2.1.0-2ce4ab214f-b377ec5a46.zip differ
diff --git a/.yarn/cache/tree-changes-npm-0.11.2-eb720d8e1c-bcdcf9b550.zip b/.yarn/cache/tree-changes-npm-0.11.2-eb720d8e1c-bcdcf9b550.zip
new file mode 100644
index 0000000..8db0242
Binary files /dev/null and b/.yarn/cache/tree-changes-npm-0.11.2-eb720d8e1c-bcdcf9b550.zip differ
diff --git a/.yarn/cache/tree-changes-npm-0.9.3-e337138b32-e7a38ed3c2.zip b/.yarn/cache/tree-changes-npm-0.9.3-e337138b32-e7a38ed3c2.zip
new file mode 100644
index 0000000..0d118a2
Binary files /dev/null and b/.yarn/cache/tree-changes-npm-0.9.3-e337138b32-e7a38ed3c2.zip differ
diff --git a/.yarn/cache/type-fest-npm-4.8.3-5f50002256-978edf5d00.zip b/.yarn/cache/type-fest-npm-4.8.3-5f50002256-978edf5d00.zip
new file mode 100644
index 0000000..67025bb
Binary files /dev/null and b/.yarn/cache/type-fest-npm-4.8.3-5f50002256-978edf5d00.zip differ
diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz
index f5e55eb..2e30fbc 100644
Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ
diff --git a/packages/admin/src/App.tsx b/packages/admin/src/App.tsx
index b07b255..80e3c5b 100644
--- a/packages/admin/src/App.tsx
+++ b/packages/admin/src/App.tsx
@@ -5,6 +5,7 @@ import Board from './components/Board/Board.tsx';
import Nav from './components/Nav.tsx';
import SystemInfo from './components/SystemInfo/SystemInfo.tsx';
import ExceptionStatistics from './components/ExceptionStatistics/ExceptionStatistics.tsx';
+import Login from './components/Login/Login.tsx';
function App() {
return (
@@ -14,6 +15,7 @@ function App() {
Board
System Info
Exception Statistics
+ Login
Home} />
@@ -23,6 +25,7 @@ function App() {
path="/admin/exception-statistics"
element={}
/>
+ } />
>
);
diff --git a/packages/admin/src/components/Board/Board.tsx b/packages/admin/src/components/Board/Board.tsx
index 46de0cc..81f6fdf 100644
--- a/packages/admin/src/components/Board/Board.tsx
+++ b/packages/admin/src/components/Board/Board.tsx
@@ -10,6 +10,10 @@ export default function Board() {
const getBoardList = async () => {
const response = await fetch(baseUrl + '/admin/post');
+ if (response.status === 401) {
+ // 로그인 페이지로 리다이렉트
+ window.location.href = '/admin/login';
+ }
const data = await response.json();
setBoardList(data);
};
@@ -38,28 +42,27 @@ export default function Board() {
- {boardList.map((board: any) => (
-
- {board.id} |
- {board.title} |
- {board.user.nickname} |
- {board.like_cnt} |
- {board.images.length} |
- {board.created_at} |
- {board.updated_at} |
-
-
- |
-
- ))}
+ {boardList &&
+ boardList.map((board: any) => (
+
+ {board.id} |
+ {board.title} |
+ {board.user.nickname} |
+ {board.like_cnt} |
+ {board.images.length} |
+ {board.created_at} |
+ {board.updated_at} |
+
+
+ |
+
+ ))}
{boardDetail &&
Object.keys(boardDetail)?.map((detail: any) => {
- return (
-
{detail + ' | ' + boardDetail[detail]}
- );
+ return
{detail + ' | ' + boardDetail[detail]}
;
})}
diff --git a/packages/admin/src/components/ExceptionStatistics/ExceptionStatistics.tsx b/packages/admin/src/components/ExceptionStatistics/ExceptionStatistics.tsx
index 7870211..409661c 100644
--- a/packages/admin/src/components/ExceptionStatistics/ExceptionStatistics.tsx
+++ b/packages/admin/src/components/ExceptionStatistics/ExceptionStatistics.tsx
@@ -11,6 +11,10 @@ export default function ExceptionStatistics() {
const getAllExceptions = async () => {
const response = await fetch(baseUrl + '/admin/exception');
+ if (response.status === 401) {
+ // 로그인 페이지로 리다이렉트
+ window.location.href = '/admin/login';
+ }
const exceptions = await response.json();
exceptions.map((exception: Exception) => {
if (exception.path.includes('?')) {
diff --git a/packages/admin/src/components/Login/Login.tsx b/packages/admin/src/components/Login/Login.tsx
new file mode 100644
index 0000000..d6b01b2
--- /dev/null
+++ b/packages/admin/src/components/Login/Login.tsx
@@ -0,0 +1,48 @@
+import { useState } from 'react';
+
+const baseUrl = import.meta.env.VITE_API_BASE_URL;
+
+export default function Login() {
+ const [password, setPassword] = useState('');
+ const [loginResult, setLoginResult] = useState('');
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setPassword(e.target.value);
+ };
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const response = await fetch(baseUrl + '/admin/signin', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ password }),
+ });
+
+ if (response.ok) {
+ setLoginResult('로그인 성공');
+ } else {
+ setLoginResult('로그인 실패');
+ }
+ };
+
+ const handleLogout = async () => {
+ const response = await fetch(baseUrl + '/admin/signout');
+ if (response.ok) {
+ setLoginResult('로그아웃 되었습니다.');
+ }
+ };
+
+ return (
+
+
+ {loginResult}
+
+
+ );
+}
diff --git a/packages/admin/src/components/SystemInfo/SystemInfo.tsx b/packages/admin/src/components/SystemInfo/SystemInfo.tsx
index 4740c8a..5e7ffe0 100644
--- a/packages/admin/src/components/SystemInfo/SystemInfo.tsx
+++ b/packages/admin/src/components/SystemInfo/SystemInfo.tsx
@@ -9,6 +9,10 @@ export default function SystemInfo() {
const getSystemInfo = async () => {
const response = await fetch(baseUrl + '/admin/system-info');
+ if (response.status === 401) {
+ // 로그인 페이지로 리다이렉트
+ window.location.href = '/admin/login';
+ }
const data = await response.json();
setSystemInfo(data);
};
@@ -38,24 +42,22 @@ export default function SystemInfo() {
{systemInfo.diskUsageHead &&
- systemInfo.diskUsageHead?.map(
- (head: string, index: number) => {head} | ,
- )}
+ systemInfo.diskUsageHead?.map((head: string, index: number) => (
+ {head} |
+ ))}
{systemInfo.diskUsage &&
- systemInfo.diskUsage?.map(
- (line: string[], index: number) => {
- return (
-
- {line.map((item: string, index: number) => (
- {item} |
- ))}
-
- );
- },
- )}
+ systemInfo.diskUsage?.map((line: string[], index: number) => {
+ return (
+
+ {line.map((item: string, index: number) => (
+ {item} |
+ ))}
+
+ );
+ })}
diff --git a/packages/client/package.json b/packages/client/package.json
index e9d5c63..c92ee8e 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -21,6 +21,7 @@
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-full-screen": "^1.1.1",
+ "react-joyride": "^2.7.1",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.18.0",
"remark-gfm": "^4.0.0",
diff --git a/packages/client/src/app/App.tsx b/packages/client/src/app/App.tsx
index 4b8b4df..67a7c79 100644
--- a/packages/client/src/app/App.tsx
+++ b/packages/client/src/app/App.tsx
@@ -6,11 +6,15 @@ import { RouterProvider } from 'react-router-dom';
import { ThemeProvider } from '@emotion/react';
import theme from 'shared/ui/styles/theme';
import './global.css';
-import { AxiosInterceptor } from 'shared/apis/AxiosInterceptor';
+import { AxiosInterceptor } from 'shared/apis/core/AxiosInterceptor';
+import Audio from 'features/audio/Audio';
+import AudioButton from 'shared/ui/audioButton/AudioButton';
ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
diff --git a/packages/client/src/app/Router.tsx b/packages/client/src/app/Router.tsx
index c71a6ee..f870cbd 100644
--- a/packages/client/src/app/Router.tsx
+++ b/packages/client/src/app/Router.tsx
@@ -5,7 +5,7 @@ import {
} from 'react-router-dom';
import Home from '../pages/Home/Home';
import Landing from '../pages/Landing/Landing';
-import { WritingModal } from 'features/writingModal/WritingModal';
+import { WritingModal } from 'features/writingModal';
import LoginModal from 'widgets/loginModal/LoginModal';
import SignUpModal from 'widgets/signupModal/SignUpModal';
import NickNameSetModal from 'widgets/nickNameSetModal/NickNameSetModal';
@@ -14,28 +14,56 @@ import { PostModal } from 'features/postModal';
import GalaxyCustomModal from 'widgets/galaxyCustomModal/GalaxyCustomModal';
import StarCustomModal from 'widgets/starCustomModal/StarCustomModal';
import ShareModal from 'widgets/shareModal/ShareModal';
+import PrivateRoute from '../shared/routes/PrivateRoute';
+import PublicRoute from 'shared/routes/PublicRoute';
+import FallBackComponent from 'widgets/error/FallBackComponent';
export const router = createBrowserRouter(
createRoutesFromElements(
<>
}>
} />
- } />
- } />
- }>
-
+
+ }>
+ } />
+ } />
+ }>
+
+
+
+
+
+ }>
+ }>
+
+ } />
+
+ } />
+ } />
+ } />
+ } />
- }>
-
- } />
+ }>
+
+
+ } />
+
- } />
- } />
- } />
- } />
+
+ }>
+ }>
+
+
+ } />
+
+
+
+
+
+ } />
>,
),
);
diff --git a/packages/client/src/app/global.css b/packages/client/src/app/global.css
index 53c8af3..dd9755c 100644
--- a/packages/client/src/app/global.css
+++ b/packages/client/src/app/global.css
@@ -16,3 +16,44 @@ li {
a {
text-decoration: none;
}
+
+body {
+ overflow: hidden;
+ font-family:
+ Pretendard,
+ -apple-system,
+ BlinkMacSystemFont,
+ system-ui,
+ Roboto,
+ 'Helvetica Neue',
+ 'Segoe UI',
+ 'Apple SD Gothic Neo',
+ 'Noto Sans KR',
+ 'Malgun Gothic',
+ 'Apple Color Emoji',
+ 'Segoe UI Emoji',
+ 'Segoe UI Symbol',
+ sans-serif;
+}
+
+@font-face {
+ font-family: 'pretendard_medium';
+ src:
+ url('/assets/fonts/Pretendard-Medium.woff2') format('woff2'),
+ url('/assets/fonts/Pretendard-Medium.woff') format('woff'),
+ url('/assets/fonts/Pretendard-Medium.otf') format('otf');
+ font-display: swap;
+ font-weight: 500;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'pretendard_bold';
+ src:
+ url('/assets/fonts/Pretendard-Bold.woff2') format('woff2'),
+ url('/assets/fonts/Pretendard-Bold.woff') format('woff'),
+ url('/assets/fonts/Pretendard-Bold.otf') format('otf');
+ font-display: swap;
+ font-weight: 700;
+ font-style: normal;
+}
diff --git a/packages/client/src/assets/fonts/Pretendard-Bold.otf b/packages/client/src/assets/fonts/Pretendard-Bold.otf
new file mode 100644
index 0000000..8e5e30a
Binary files /dev/null and b/packages/client/src/assets/fonts/Pretendard-Bold.otf differ
diff --git a/packages/client/src/assets/fonts/Pretendard-Bold.woff b/packages/client/src/assets/fonts/Pretendard-Bold.woff
new file mode 100644
index 0000000..7837ae5
Binary files /dev/null and b/packages/client/src/assets/fonts/Pretendard-Bold.woff differ
diff --git a/packages/client/src/assets/fonts/Pretendard-Bold.woff2 b/packages/client/src/assets/fonts/Pretendard-Bold.woff2
new file mode 100644
index 0000000..4d40a1a
Binary files /dev/null and b/packages/client/src/assets/fonts/Pretendard-Bold.woff2 differ
diff --git a/packages/client/src/assets/fonts/Pretendard-Medium.otf b/packages/client/src/assets/fonts/Pretendard-Medium.otf
new file mode 100644
index 0000000..0575069
Binary files /dev/null and b/packages/client/src/assets/fonts/Pretendard-Medium.otf differ
diff --git a/packages/client/src/assets/fonts/Pretendard-Medium.woff b/packages/client/src/assets/fonts/Pretendard-Medium.woff
new file mode 100644
index 0000000..5370409
Binary files /dev/null and b/packages/client/src/assets/fonts/Pretendard-Medium.woff differ
diff --git a/packages/client/src/assets/fonts/Pretendard-Medium.woff2 b/packages/client/src/assets/fonts/Pretendard-Medium.woff2
new file mode 100644
index 0000000..f8c743d
Binary files /dev/null and b/packages/client/src/assets/fonts/Pretendard-Medium.woff2 differ
diff --git a/packages/client/src/assets/icons/icon-add-24-gray.svg b/packages/client/src/assets/icons/icon-add-24-gray.svg
new file mode 100644
index 0000000..5a15629
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-add-24-gray.svg
@@ -0,0 +1,11 @@
+
diff --git a/packages/client/src/assets/icons/icon-add-24-white.svg b/packages/client/src/assets/icons/icon-add-24-white.svg
new file mode 100644
index 0000000..40a68f5
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-add-24-white.svg
@@ -0,0 +1,11 @@
+
diff --git a/packages/client/src/assets/icons/icon-arrow-left-32-white.svg b/packages/client/src/assets/icons/icon-arrow-left-32-white.svg
new file mode 100644
index 0000000..a2240ec
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-arrow-left-32-white.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/icons/icon-arrow-right-17-white.svg b/packages/client/src/assets/icons/icon-arrow-right-17-white.svg
new file mode 100644
index 0000000..acde03c
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-arrow-right-17-white.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/client/src/assets/icons/icon-arrow-right-32-white.svg b/packages/client/src/assets/icons/icon-arrow-right-32-white.svg
new file mode 100644
index 0000000..4494bfe
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-arrow-right-32-white.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/icons/icon-back-32-white.svg b/packages/client/src/assets/icons/icon-back-32-white.svg
new file mode 100644
index 0000000..d3b3cda
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-back-32-white.svg
@@ -0,0 +1,11 @@
+
diff --git a/packages/client/src/assets/icons/icon-check-purple.svg b/packages/client/src/assets/icons/icon-check-purple.svg
new file mode 100644
index 0000000..022251f
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-check-purple.svg
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/packages/client/src/assets/icons/icon-confirm-22.svg b/packages/client/src/assets/icons/icon-confirm-22.svg
new file mode 100644
index 0000000..e3ebce9
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-confirm-22.svg
@@ -0,0 +1,11 @@
+
diff --git a/packages/client/src/assets/icons/icon-eraser-24-active.svg b/packages/client/src/assets/icons/icon-eraser-24-active.svg
new file mode 100644
index 0000000..d6c7295
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-eraser-24-active.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/icons/icon-eraser-24-default.svg b/packages/client/src/assets/icons/icon-eraser-24-default.svg
new file mode 100644
index 0000000..5041f4a
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-eraser-24-default.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/icons/icon-naver-16-white.svg b/packages/client/src/assets/icons/icon-naver-16-white.svg
new file mode 100644
index 0000000..5ba1676
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-naver-16-white.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/icons/icon-pen-24-active.svg b/packages/client/src/assets/icons/icon-pen-24-active.svg
new file mode 100644
index 0000000..b1af8c4
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-pen-24-active.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/icons/icon-pen-24-default.svg b/packages/client/src/assets/icons/icon-pen-24-default.svg
new file mode 100644
index 0000000..caae3ee
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-pen-24-default.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/icons/icon-photo-32-gray.svg b/packages/client/src/assets/icons/icon-photo-32-gray.svg
new file mode 100644
index 0000000..b88c098
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-photo-32-gray.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/icons/icon-planetedit-24-gray.svg b/packages/client/src/assets/icons/icon-planetedit-24-gray.svg
new file mode 100644
index 0000000..58921bc
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-planetedit-24-gray.svg
@@ -0,0 +1,12 @@
+
diff --git a/packages/client/src/assets/icons/icon-planetedit-24-white.svg b/packages/client/src/assets/icons/icon-planetedit-24-white.svg
new file mode 100644
index 0000000..3eeabf3
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-planetedit-24-white.svg
@@ -0,0 +1,12 @@
+
diff --git a/packages/client/src/assets/icons/icon-search-24-white.svg b/packages/client/src/assets/icons/icon-search-24-white.svg
new file mode 100644
index 0000000..1c3e933
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-search-24-white.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/icons/icon-shuffle-24-gray.svg b/packages/client/src/assets/icons/icon-shuffle-24-gray.svg
new file mode 100644
index 0000000..9becce1
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-shuffle-24-gray.svg
@@ -0,0 +1,11 @@
+
diff --git a/packages/client/src/assets/icons/icon-shuffle-24-white.svg b/packages/client/src/assets/icons/icon-shuffle-24-white.svg
new file mode 100644
index 0000000..50cb879
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-shuffle-24-white.svg
@@ -0,0 +1,11 @@
+
diff --git a/packages/client/src/assets/icons/icon-writte-24-gray.svg b/packages/client/src/assets/icons/icon-writte-24-gray.svg
new file mode 100644
index 0000000..6b2033e
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-writte-24-gray.svg
@@ -0,0 +1,11 @@
+
diff --git a/packages/client/src/assets/icons/icon-writte-24-white.svg b/packages/client/src/assets/icons/icon-writte-24-white.svg
new file mode 100644
index 0000000..4383af9
--- /dev/null
+++ b/packages/client/src/assets/icons/icon-writte-24-white.svg
@@ -0,0 +1,10 @@
+
diff --git a/packages/client/src/assets/logos/404.png b/packages/client/src/assets/logos/404.png
new file mode 100644
index 0000000..8da2774
Binary files /dev/null and b/packages/client/src/assets/logos/404.png differ
diff --git a/packages/client/src/assets/logos/Github.png b/packages/client/src/assets/logos/Github.png
new file mode 100644
index 0000000..50b8175
Binary files /dev/null and b/packages/client/src/assets/logos/Github.png differ
diff --git a/packages/client/src/assets/logos/Google.png b/packages/client/src/assets/logos/Google.png
new file mode 100644
index 0000000..9549e81
Binary files /dev/null and b/packages/client/src/assets/logos/Google.png differ
diff --git a/packages/client/src/assets/logos/Naver.png b/packages/client/src/assets/logos/Naver.png
new file mode 100644
index 0000000..e3c3242
Binary files /dev/null and b/packages/client/src/assets/logos/Naver.png differ
diff --git a/packages/client/src/assets/logos/favicon.png b/packages/client/src/assets/logos/favicon.png
new file mode 100644
index 0000000..282c115
Binary files /dev/null and b/packages/client/src/assets/logos/favicon.png differ
diff --git a/packages/client/src/assets/logos/logo.png b/packages/client/src/assets/logos/logo.png
new file mode 100644
index 0000000..0ab8909
Binary files /dev/null and b/packages/client/src/assets/logos/logo.png differ
diff --git a/packages/client/src/assets/musics/bgm.mp3 b/packages/client/src/assets/musics/bgm.mp3
new file mode 100644
index 0000000..91a030c
Binary files /dev/null and b/packages/client/src/assets/musics/bgm.mp3 differ
diff --git a/packages/client/src/entities/like/Like.tsx b/packages/client/src/entities/like/Like.tsx
index 5230010..afe400a 100644
--- a/packages/client/src/entities/like/Like.tsx
+++ b/packages/client/src/entities/like/Like.tsx
@@ -2,7 +2,7 @@ import { useEffect, useReducer, useState } from 'react';
import { Heart } from 'lucide-react';
import { Button } from 'shared/ui';
import theme from 'shared/ui/styles/theme';
-import instance from 'shared/apis/AxiosInterceptor';
+import instance from 'shared/apis/core/AxiosInterceptor';
interface PropsType {
postId: string;
diff --git a/packages/client/src/entities/posts/apis/getMyPost.ts b/packages/client/src/entities/posts/apis/getMyPost.ts
index 7a03c92..ed15e94 100644
--- a/packages/client/src/entities/posts/apis/getMyPost.ts
+++ b/packages/client/src/entities/posts/apis/getMyPost.ts
@@ -1,6 +1,7 @@
-import instance from 'shared/apis/AxiosInterceptor';
+import instance from 'shared/apis/core/AxiosInterceptor';
+import { StarData } from 'shared/lib/types/star';
-export const getMyPost = async () => {
+export const getMyPost = async (): Promise => {
const { data } = await instance({
method: 'GET',
url: '/star',
diff --git a/packages/client/src/entities/posts/ui/Post.tsx b/packages/client/src/entities/posts/ui/Post.tsx
index 928449d..7ccdda7 100644
--- a/packages/client/src/entities/posts/ui/Post.tsx
+++ b/packages/client/src/entities/posts/ui/Post.tsx
@@ -6,7 +6,7 @@ import styled from '@emotion/styled';
import { useViewStore } from 'shared/store/useViewStore';
import * as THREE from 'three';
import { StarType } from 'shared/lib/types/star';
-import { useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import { useState } from 'react';
import theme from 'shared/ui/styles/theme';
import Star from 'features/star/Star';
@@ -19,25 +19,32 @@ interface PropsType {
export default function Post({ data, postId, title }: PropsType) {
const { targetView, setTargetView } = useCameraStore();
- const { view, setView } = useViewStore();
+ const { setView } = useViewStore();
const meshRef = useRef(null!);
const [isHovered, setIsHovered] = useState(false);
const navigate = useNavigate();
+ const location = useLocation();
const handleMeshClick = (e: ThreeEvent) => {
e.stopPropagation();
+ const splitedPath = location.pathname.split('/');
+ const page = splitedPath[1];
+ const nickName = splitedPath[2];
+ let path = '/';
+ if (page === 'home') path += page + '/';
+ else path += page + '/' + nickName + '/';
+
if (meshRef.current !== targetView) {
setView('DETAIL');
setTargetView(meshRef.current);
- navigate(`/home/${postId}`);
- return;
+ return navigate(path + postId);
}
setView('POST');
- navigate(`/home/${postId}/detail`);
+ navigate(path + postId + '/detail');
};
const handlePointerOver = (e: ThreeEvent) => {
@@ -64,7 +71,7 @@ export default function Post({ data, postId, title }: PropsType) {
brightness={data.brightness}
shape={data.shape}
>
- {view !== 'POST' && isHovered && (
+ {isHovered && (
diff --git a/packages/client/src/entities/posts/ui/Posts.tsx b/packages/client/src/entities/posts/ui/Posts.tsx
index 630b748..633a0ea 100644
--- a/packages/client/src/entities/posts/ui/Posts.tsx
+++ b/packages/client/src/entities/posts/ui/Posts.tsx
@@ -1,33 +1,27 @@
import Post from './Post';
import { useState, useEffect } from 'react';
import { StarData } from 'shared/lib/types/star';
-import { useOwnerStore } from 'shared/store/useOwnerStore';
import { getPostListByNickName } from 'shared/apis/star';
-import { useViewStore } from 'shared/store';
import { getMyPost } from '../apis/getMyPost';
+import useCheckNickName from 'shared/hooks/useCheckNickName';
+import { useViewStore } from 'shared/store';
export default function Posts() {
const [postData, setPostData] = useState();
-
- const { isMyPage, pageOwnerNickName } = useOwnerStore();
const { view } = useViewStore();
+ const { page, nickName } = useCheckNickName();
+
useEffect(() => {
- if (view !== 'MAIN') return;
+ if (!page) return;
+ if (!nickName) return;
- if (isMyPage) {
- (async () => {
- const myPostData = await getMyPost();
- setPostData(myPostData);
- })();
- return;
+ if (page === 'home') {
+ getMyPost().then((res) => setPostData(res));
+ } else {
+ getPostListByNickName(nickName).then((res) => setPostData(res));
}
-
- (async () => {
- const otherPostData = await getPostListByNickName(pageOwnerNickName);
- setPostData(otherPostData);
- })();
- }, [view, pageOwnerNickName]);
+ }, [view, nickName]);
return (
diff --git a/packages/client/src/features/coachMarker/CoachButton.tsx b/packages/client/src/features/coachMarker/CoachButton.tsx
new file mode 100644
index 0000000..bdc0b1e
--- /dev/null
+++ b/packages/client/src/features/coachMarker/CoachButton.tsx
@@ -0,0 +1,19 @@
+import { HelpCircleIcon } from 'lucide-react';
+import { useState } from 'react';
+import CoachMarker from './CoachMarker';
+import styled from '@emotion/styled';
+
+export default function CoachButton() {
+ const [showCoach, setShowCoach] = useState(false);
+ return (
+ <>
+ setShowCoach(!showCoach)} />
+ {showCoach && }
+ >
+ );
+}
+
+const HelpCircleButton = styled(HelpCircleIcon)`
+ color: ${({ theme }) => theme.colors.text.secondary};
+ cursor: pointer;
+`;
diff --git a/packages/client/src/features/coachMarker/CoachMarker.tsx b/packages/client/src/features/coachMarker/CoachMarker.tsx
new file mode 100644
index 0000000..69e9f3d
--- /dev/null
+++ b/packages/client/src/features/coachMarker/CoachMarker.tsx
@@ -0,0 +1,114 @@
+import Joyride, { Step } from 'react-joyride';
+import { patchShareStatus } from 'shared/apis/share';
+
+interface PropsType {
+ isFirst: boolean;
+}
+
+export default function CoachMarker({ isFirst }: PropsType) {
+ const steps: Step[] = [
+ {
+ target: 'body',
+ content: (
+
+
별 하나에 글 하나 🌟
+
+
"내 삶의 반짝이는 기억들을 우주에 담아보세요"
+
+
우리는 모두 형형색색의 기억들을 가지고 있습니다.
+
+
그 기억들을 눈으로 볼 수 있다면 얼마나 좋을까요?
+
+
이곳, 나만의 우주에서 기억을 별로 만들어 띄워보아요. ☺️
+
+ ),
+ disableBeacon: true,
+ placement: 'center',
+ styles: {
+ options: {
+ width: 500,
+ },
+ },
+ },
+ {
+ target: '.leva',
+ content: (
+
+
우주 커스텀 🌌
+
+
+ 은하의 밝기,우주 블러효과 유무, 마우스의 휠 스피드를 변경할 수
+ 있습니다.
+
+
우주를 나에게 맞게 수정해보세요.
+
+ ),
+ },
+ {
+ target: '.search-bar',
+ content: (
+
+
우주 검색 🔎
+
+
다른 사람의 우주를 검색할 수 있습니다.
+
각양각색의 기억들을 함께 탐험해볼까요?
+
+ ),
+ },
+ {
+ target: '.writing-button',
+ content: (
+
+
별 생성 ⭐️
+
+
글을 작성하여 우주의 별로 띄울 수 있습니다.
+
내 별의 모양, 색상, 밝기, 크기를 골라보아요.
+
나의 기분에 맞는 색도 추천받을 수 있어요.
+
+ ),
+ },
+ {
+ target: '.galaxy-custom-button',
+ content: (
+
+
은하 커스텀 💫
+
+
은하의 모양을 바꿀 수 있습니다.
+
나만의 개성있는 은하를 만들어봐요!
+
+ ),
+ },
+ {
+ target: '.share-button',
+ content: (
+
+
우주 공유 🔗
+
+
우주를 비공개로 설정하거나, 다른 사람에게 공유할 수 있습니다.
+
혼자만의 비밀 이야기를 담아도 좋아요.
+
사랑하는 사람과 함께하는 것도 좋아요. 🫶🏻
+
+ ),
+ },
+ ];
+ return (
+ {
+ if (data.type === 'tour:end' && isFirst) patchShareStatus('public');
+ }}
+ />
+ );
+}
diff --git a/packages/client/src/features/controls/Controls.tsx b/packages/client/src/features/controls/Controls.tsx
index c512270..6a73b4a 100644
--- a/packages/client/src/features/controls/Controls.tsx
+++ b/packages/client/src/features/controls/Controls.tsx
@@ -7,7 +7,7 @@ import type { OrbitControls as OrbitControlsImpl } from 'three-stdlib';
import { useViewStore } from 'shared/store/useViewStore';
import { useEffect } from 'react';
import { CAMERA_POST_VIEW } from '@constants';
-import { useOwnerStore } from 'shared/store/useOwnerStore';
+import useCheckNickName from 'shared/hooks/useCheckNickName';
const setCameraPosition = (
camera: Camera,
@@ -52,7 +52,7 @@ export default function Controls() {
} = useCameraStore();
const { view } = useViewStore();
const state = useThree();
- const { pageOwnerNickName } = useOwnerStore();
+ const { nickName } = useCheckNickName();
useEffect(() => {
setCameraToCurrentView(currentView.distanceTo(state.camera.position));
@@ -60,7 +60,7 @@ export default function Controls() {
useEffect(() => {
setTargetView(null);
- }, [pageOwnerNickName]);
+ }, [nickName]);
useFrame((state, delta) => {
const targetPosition = new THREE.Vector3(0, 0, 0);
diff --git a/packages/client/src/features/postModal/api/deletePost.ts b/packages/client/src/features/postModal/api/deletePost.ts
index 5ae3f11..b2d4941 100644
--- a/packages/client/src/features/postModal/api/deletePost.ts
+++ b/packages/client/src/features/postModal/api/deletePost.ts
@@ -1,4 +1,4 @@
-import instance from 'shared/apis/AxiosInterceptor';
+import instance from 'shared/apis/core/AxiosInterceptor';
export const deletePost = async (postId: string) => {
const res = await instance({
diff --git a/packages/client/src/features/postModal/ui/ImageSlider.tsx b/packages/client/src/features/postModal/ui/ImageSlider.tsx
index 29804f2..84d01d8 100644
--- a/packages/client/src/features/postModal/ui/ImageSlider.tsx
+++ b/packages/client/src/features/postModal/ui/ImageSlider.tsx
@@ -1,5 +1,7 @@
import { useState } from 'react';
-import { ArrowBigLeft, ArrowBigRight, CircleDot, Circle } from 'lucide-react';
+import { CircleDot, Circle } from 'lucide-react';
+import ArrowBigLeft from '@icons/icon-arrow-left-32-white.svg?react';
+import ArrowBigRight from '@icons/icon-arrow-right-32-white.svg?react';
import styled from '@emotion/styled';
interface PropsType {
@@ -88,7 +90,7 @@ const Button = styled.button`
position: absolute;
top: 0;
bottom: 0;
- padding: 16px;
+ padding: 8px;
cursor: pointer;
transition: background-color 100ms ease-in-out;
diff --git a/packages/client/src/features/postModal/ui/PostModal.tsx b/packages/client/src/features/postModal/ui/PostModal.tsx
index f772481..eae22af 100644
--- a/packages/client/src/features/postModal/ui/PostModal.tsx
+++ b/packages/client/src/features/postModal/ui/PostModal.tsx
@@ -5,26 +5,38 @@ import remarkGfm from 'remark-gfm';
import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import AlertDialog from 'shared/ui/alertDialog/AlertDialog';
-import { useNavigate, useParams } from 'react-router-dom';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useFetch } from 'shared/hooks';
import { PostData } from 'shared/lib/types/post';
import { deletePost } from '../api/deletePost';
import ImageSlider from './ImageSlider';
import Like from 'entities/like/Like';
import InputBar from 'shared/ui/inputBar/InputBar';
-import instance from 'shared/apis/AxiosInterceptor';
-import { useToastStore } from 'shared/store';
+import instance from 'shared/apis/core/AxiosInterceptor';
+import { useToastStore, useCameraStore } from 'shared/store';
+import useCheckNickName from 'shared/hooks/useCheckNickName';
+import { useRefresh } from 'shared/hooks/useRefresh';
export default function PostModal() {
- const { setView } = useViewStore();
const [deleteModal, setDeleteModal] = useState(false);
- const { postId } = useParams();
- const navigate = useNavigate();
- const { data, refetch } = useFetch(`post/${postId}`);
const [isEdit, setIsEdit] = useState(false);
const [content, setContent] = useState('');
const [title, setTitle] = useState('');
- const { setText } = useToastStore();
+ const [isDeleteButtonDisabled, setIsDeleteButtonDisabled] = useState(false);
+ const [isSaveButtonDisabled, setIsSaveButtonDisabled] = useState(false);
+
+ const { setToast } = useToastStore();
+ const { setView } = useViewStore();
+ const { setTargetView } = useCameraStore();
+ const { postId } = useParams();
+ const { data, refetch } = useFetch(`post/${postId}`);
+
+ const { page } = useCheckNickName();
+
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ useRefresh('POST');
useEffect(() => {
setContent(data?.content ?? '');
@@ -32,20 +44,23 @@ export default function PostModal() {
}, [data]);
const handleEditSave = async () => {
+ if (isSaveButtonDisabled) return;
+ setIsSaveButtonDisabled(true);
const formData = new FormData();
formData.append('title', title);
formData.append('content', content);
- const res = await instance({
- url: `/post/${postId}`,
- method: 'PATCH',
- data: formData,
- });
- if (res.status === 200) {
+
+ try {
+ await instance({
+ url: `/post/${postId}`,
+ method: 'PATCH',
+ data: formData,
+ });
setIsEdit(false);
- setText('글이 수정되었습니다.');
+ setToast({ text: '글이 수정되었습니다.', type: 'success' });
refetch();
- } else {
- setText('글 수정에 실패했습니다.');
+ } finally {
+ setIsSaveButtonDisabled(false);
}
};
@@ -57,6 +72,7 @@ export default function PostModal() {
onClick={() => {
setDeleteModal(true);
}}
+ disabled={page !== 'home'}
>
삭제
@@ -70,6 +86,7 @@ export default function PostModal() {
onClick={() => {
setIsEdit(true);
}}
+ disabled={page !== 'home'}
>
수정
@@ -97,24 +114,37 @@ export default function PostModal() {
setIsEdit(false);
handleEditSave();
}}
+ disabled={isSaveButtonDisabled}
>
저장
);
const handleDelete = async () => {
- const res = await deletePost(postId!);
- setDeleteModal(false);
- if (res.status === 200) {
- setText('글을 삭제했습니다.');
+ try {
+ await deletePost(postId!);
+ setDeleteModal(false);
+ setToast({ text: '글이 삭제되었습니다.', type: 'success' });
setView('MAIN');
navigate('/home');
- // window.location.reload();
- } else {
- setText('글 삭제에 실패했습니다.');
+ } finally {
+ setIsDeleteButtonDisabled(false);
}
};
+ const handleGoBackButton = () => {
+ const splitedPath = location.pathname.split('/');
+ const page = splitedPath[1];
+ const nickName = splitedPath[2];
+ let path = '/';
+ if (page === 'home') path += page + '/';
+ else path += page + '/' + nickName + '/';
+
+ setView('MAIN');
+ navigate(path);
+ setTargetView(null);
+ };
+
return (
data && (
@@ -125,10 +155,7 @@ export default function PostModal() {
leftButton={
isEdit ? null :
}
- onClickGoBack={() => {
- setView('MAIN');
- navigate(`/home/${postId}`);
- }}
+ onClickGoBack={handleGoBackButton}
>
{data.images.length > 0 && !isEdit && (
@@ -173,6 +200,7 @@ export default function PostModal() {
setDeleteModal(false);
}}
onClickActionButton={handleDelete}
+ disabled={isDeleteButtonDisabled}
/>
)}
@@ -209,6 +237,24 @@ const TextContainer = styled.div`
${({ theme: { colors } }) => ({
color: colors.text.secondary,
})}
+
+ & ol {
+ padding-left: 40px;
+ margin: 18px 0;
+ }
+
+ & ul {
+ padding-left: 40px;
+ margin: 18px 0;
+ }
+
+ & ol li {
+ list-style: decimal;
+ }
+
+ & ul li {
+ list-style: disc;
+ }
`;
const ImageContainer = styled.div`
diff --git a/packages/client/src/features/writingModal/WritingModal.tsx b/packages/client/src/features/writingModal/index.ts
similarity index 100%
rename from packages/client/src/features/writingModal/WritingModal.tsx
rename to packages/client/src/features/writingModal/index.ts
diff --git a/packages/client/src/features/writingModal/ui/Images.tsx b/packages/client/src/features/writingModal/ui/Images.tsx
index e1e3979..5a32b03 100644
--- a/packages/client/src/features/writingModal/ui/Images.tsx
+++ b/packages/client/src/features/writingModal/ui/Images.tsx
@@ -1,6 +1,5 @@
import ImageIcon from '@icons/icon-photo-32-gray.svg?react';
import { useState, ChangeEvent, useRef } from 'react';
-import { IconButton } from 'shared/ui';
import styled from '@emotion/styled';
interface PropsType {
@@ -15,6 +14,7 @@ export default function Images({ onModify }: PropsType) {
const handleAddImages = (event: ChangeEvent) => {
const files = event.target.files;
if (!files) return;
+ if (files.length > 5) return;
const images = Array.from(files).map((file) => URL.createObjectURL(file));
setShowImages(images);
setFiles(files);
@@ -43,9 +43,9 @@ export default function Images({ onModify }: PropsType) {
hidden
ref={inputRef}
/>
- inputRef.current?.click()}>
+ inputRef.current?.click()}>
-
+
{showImages.map((image, id) => (
@@ -61,10 +61,13 @@ export default function Images({ onModify }: PropsType) {
}
const ImagePreview = styled.img`
- width: 40px;
- height: 40px;
- border-radius: 8px;
+ width: 80px;
+ height: 80px;
+ border-radius: 4px;
cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
`;
const Container = styled.div`
@@ -73,3 +76,20 @@ const Container = styled.div`
align-items: center;
gap: 12px;
`;
+
+const IconWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 80px;
+ height: 80px;
+ border: 1px solid ${({ theme }) => theme.colors.stroke.default};
+ border-radius: 4px;
+ background-color: ${({ theme }) => theme.colors.background.bdp01_80};
+ cursor: pointer;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.primary.hover};
+ border: 1px solid ${({ theme }) => theme.colors.action.hover};
+ }
+`;
diff --git a/packages/client/src/features/writingModal/ui/WritingModal.tsx b/packages/client/src/features/writingModal/ui/WritingModal.tsx
index 329ea7c..b7b9472 100644
--- a/packages/client/src/features/writingModal/ui/WritingModal.tsx
+++ b/packages/client/src/features/writingModal/ui/WritingModal.tsx
@@ -3,24 +3,37 @@ import { Button, Modal } from 'shared/ui';
import TextArea from 'shared/ui/textArea/TextArea';
import { ModalPortal } from 'shared/ui';
import Images from './Images';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useLocation } from 'react-router-dom';
import { useViewStore, usePostStore } from 'shared/store';
import InputBar from 'shared/ui/inputBar/InputBar';
import styled from '@emotion/styled';
import { css } from '@emotion/react';
import { Caption } from 'shared/ui/styles';
+import { useRefresh } from 'shared/hooks/useRefresh';
+import { AlertDialog } from 'shared/ui';
type TextStateTypes = 'DEFAULT' | 'INVALID';
export default function WritingModal() {
- const [title, setTitle] = useState('');
- const [content, setContent] = useState('');
- const [files, setFiles] = useState
(null);
const [titleState, setTitleState] = useState('DEFAULT');
const [contentState, setContentState] = useState('DEFAULT');
const navigate = useNavigate();
const { setView } = useViewStore();
- const { setStoreTitle, setStoreContent, setStoreFiles } = usePostStore();
+ const {
+ title: storeTitle,
+ setStoreTitle,
+ content: storeContent,
+ setStoreContent,
+ files: storeFiles,
+ setStoreFiles,
+ } = usePostStore();
+ const [isClose, setIsClose] = useState(false);
+ const star = useLocation().state?.star;
+ const [title, setTitle] = useState(star ? storeTitle : '');
+ const [content, setContent] = useState(star ? storeContent : '');
+ const [files, setFiles] = useState(star ? storeFiles : null);
+
+ useRefresh('WRITING');
const handleSendPost = async () => {
if (title === '') return setTitleState('INVALID');
@@ -28,11 +41,25 @@ export default function WritingModal() {
setStoreTitle(title);
setStoreContent(content);
setStoreFiles(files);
- navigate('/home/star-custom');
+ navigate('/home/star-custom', { state: { star } });
};
return (
+ {isClose && (
+ {
+ setView('MAIN');
+ navigate('/home');
+ }}
+ onClickCancelButton={() => setIsClose(false)}
+ disabled={false}
+ />
+ )}
}
- leftButton={}
- onClickGoBack={() => {
- setView('MAIN');
- navigate('/home');
- }}
+ onClickGoBack={() => setIsClose(true)}
>
{titleState === 'INVALID' && 제목을 입력해주세요.}
- {
- setContentState('DEFAULT');
- setContent(content);
- }}
- height="40vh"
- state={contentState}
- />
- {contentState === 'INVALID' && 내용을 입력해주세요.}
+
+
+
+
+
);
}
const TitleContainer = styled.div`
- margin-bottom: 30px;
+ margin-bottom: 16px;
`;
-const TitleInput = styled(InputBar)<{ state: TextStateTypes }>`
+const ContentContainer = styled.div<{ state: TextStateTypes }>`
+ border: 1px solid;
+ border-radius: 4px;
${({ state, theme: { colors } }) => {
if (state === 'DEFAULT') return;
@@ -100,7 +133,7 @@ const TitleInput = styled(InputBar)<{ state: TextStateTypes }>`
}};
`;
-const ContentArea = styled(TextArea)<{ state: TextStateTypes }>`
+const TitleInput = styled(InputBar)<{ state: TextStateTypes }>`
${({ state, theme: { colors } }) => {
if (state === 'DEFAULT') return;
@@ -119,8 +152,16 @@ const ContentArea = styled(TextArea)<{ state: TextStateTypes }>`
`;
const Message = styled.p`
+ position: absolute;
margin: 4px 0 0 0;
color: ${({ theme: { colors } }) => colors.text.warning};
${Caption}
`;
+
+const ImagesWrapper = styled.div`
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ margin-top: 16px;
+`;
diff --git a/packages/client/src/pages/Home/Home.tsx b/packages/client/src/pages/Home/Home.tsx
index caf8243..1ec3641 100644
--- a/packages/client/src/pages/Home/Home.tsx
+++ b/packages/client/src/pages/Home/Home.tsx
@@ -1,89 +1,64 @@
import Screen from 'widgets/screen/Screen';
-import { useViewStore } from 'shared/store/useViewStore';
-import { Outlet, useNavigate } from 'react-router-dom';
-import UnderBar from 'widgets/underBar/UnderBar';
-import UpperBar from '../../widgets/upperBar/UpperBar';
+import { Outlet } from 'react-router-dom';
import WarpScreen from 'widgets/warpScreen/WarpScreen';
import { useEffect, useState } from 'react';
-import instance from 'shared/apis/AxiosInterceptor';
-import { useScreenSwitchStore } from 'shared/store/useScreenSwitchStore';
-import Cookies from 'js-cookie';
-import { getSignInInfo } from 'shared/apis';
import { getGalaxy } from 'shared/apis';
-import { useGalaxyStore, useCustomStore } from 'shared/store';
+import { useGalaxyStore, useToastStore, useCustomStore } from 'shared/store';
import { Toast } from 'shared/ui';
-import { useToastStore } from 'shared/store';
-import { useOwnerStore } from 'shared/store/useOwnerStore';
import {
SPIRAL,
GALAXY_THICKNESS,
SPIRAL_START,
ARMS_Z_DIST,
} from 'widgets/galaxy/lib/constants';
+import useCheckNickName from 'shared/hooks/useCheckNickName';
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
-import Audio from 'features/audio/Audio';
import styled from '@emotion/styled';
import { keyframes } from '@emotion/react';
+import UnderBar from 'widgets/underBar/UnderBar';
+import UpperBar from 'widgets/upperBar/UpperBar';
+import CoachMarker from 'features/coachMarker/CoachMarker';
export default function Home() {
- const { view, setView } = useViewStore();
- const { isSwitching } = useScreenSwitchStore();
- const { text } = useToastStore();
- const { pageOwnerNickName, setPageOwnerNickName } = useOwnerStore();
- const [nickname, setNickname] = useState('');
+ const [isSwitching, setIsSwitching] = useState(false);
+ const { text, type } = useToastStore();
+ const { nickName, status } = useCheckNickName();
+
const handleFullScreen = useFullScreenHandle();
- const navigate = useNavigate();
const { setSpiral, setStart, setThickness, setZDist } = useGalaxyStore();
const custom = useCustomStore();
useEffect(() => {
- (async () => {
- try {
- const res = await instance({
- method: 'GET',
- url: `/auth/check-signin`,
- });
-
- if (res.status !== 200) navigate('/login');
- } catch (error) {
- navigate('/login');
- }
- })();
-
- getSignInInfo().then((res) => {
- Cookies.set('nickname', res.nickname);
- setNickname(res.nickname);
- setPageOwnerNickName(nickname);
- });
- getGalaxy('').then((res) => {
- const { setSpiral, setStart, setThickness, setZDist } = custom;
- if (res.spiral) setSpiral(res.spiral);
-
- if (res.start) setStart(res.start);
-
- if (res.thickness) setThickness(res.thickness);
+ setIsSwitching(true);
- if (res.zDist) setZDist(res.zDist);
- });
- }, []);
-
- useEffect(() => {
- getGalaxy(pageOwnerNickName).then((res) => {
+ if (nickName === '') return;
+ getGalaxy(nickName).then((res) => {
if (!res.spiral) setSpiral(SPIRAL);
- else setSpiral(res.spiral);
+ else {
+ setSpiral(res.spiral);
+ custom.setSpiral(res.spiral);
+ }
if (!res.start) setStart(SPIRAL_START);
- else setStart(res.start);
+ else {
+ setStart(res.start);
+ custom.setStart(res.start);
+ }
if (!res.thickness) setThickness(GALAXY_THICKNESS);
- else setThickness(res.thickness);
+ else {
+ setThickness(res.thickness);
+ custom.setThickness(res.thickness);
+ }
if (!res.zDist) setZDist(ARMS_Z_DIST);
- else setZDist(res.zDist);
+ else {
+ setZDist(res.zDist);
+ custom.setZDist(res.zDist);
+ }
});
- setView('MAIN');
- }, [pageOwnerNickName]);
+ }, [nickName]);
const keyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
@@ -105,18 +80,14 @@ export default function Home() {
return (
+ {status === 'new' && }
-
- {isSwitching && }
- {!isSwitching && }
- {text && {text}}
+ {isSwitching && }
+ {!isSwitching && }
+ {text && {text}}
- {(view === 'MAIN' || view === 'DETAIL') && (
- <>
-
-
- >
- )}
+
+
@@ -133,7 +104,7 @@ const fadeout = keyframes`
}
`;
-const WhiteScreen = styled.div`
+const FadeoutScreen = styled.div`
position: absolute;
top: 0;
left: 0;
diff --git a/packages/client/src/pages/Landing/Landing.tsx b/packages/client/src/pages/Landing/Landing.tsx
index da9bc18..3aa0f79 100644
--- a/packages/client/src/pages/Landing/Landing.tsx
+++ b/packages/client/src/pages/Landing/Landing.tsx
@@ -6,7 +6,7 @@ import { Outlet } from 'react-router-dom';
export default function Landing() {
const [mouse, setMouse] = useState([0.5, 0.5]);
- const { text } = useToastStore();
+ const { text, type } = useToastStore();
return (
- {text && {text}}
+ {text && {text}}
diff --git a/packages/client/src/shared/apis/AxiosInterceptor.ts b/packages/client/src/shared/apis/core/AxiosInterceptor.ts
similarity index 64%
rename from packages/client/src/shared/apis/AxiosInterceptor.ts
rename to packages/client/src/shared/apis/core/AxiosInterceptor.ts
index 9a5f1d5..5156489 100644
--- a/packages/client/src/shared/apis/AxiosInterceptor.ts
+++ b/packages/client/src/shared/apis/core/AxiosInterceptor.ts
@@ -1,5 +1,7 @@
import axios from 'axios';
import { useEffect } from 'react';
+import { errorMessage } from 'shared/lib/constants/error';
+import { useToastStore } from 'shared/store';
const instance = axios.create({
baseURL: 'https://www.별글.site/api/',
@@ -10,13 +12,26 @@ interface Props {
children: JSX.Element;
}
+type MethodType = 'get' | 'post' | 'patch' | 'delete';
+
function AxiosInterceptor({ children }: Props) {
+ const { setToast } = useToastStore();
+
useEffect(() => {
const responseInterceptor = instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
+ const method: MethodType = error.config.method;
+ const url: string = error.config.url.split('?')[0];
+
+ if (url === '/auth/check-signin') return Promise.reject(error);
+
+ // TODO: '/auth/is-available-nickname' 부분 처리하기
+
+ setToast({ text: errorMessage[method][url], type: 'error' });
+
console.error(error.response.data);
return Promise.reject(error);
},
diff --git a/packages/client/src/shared/apis/galaxy.ts b/packages/client/src/shared/apis/galaxy.ts
index 206d6c5..9e1d2f6 100644
--- a/packages/client/src/shared/apis/galaxy.ts
+++ b/packages/client/src/shared/apis/galaxy.ts
@@ -1,4 +1,4 @@
-import instance from './AxiosInterceptor';
+import instance from './core/AxiosInterceptor';
export const getGalaxy = async (nickname: string) => {
if (nickname !== '') {
diff --git a/packages/client/src/shared/apis/index.ts b/packages/client/src/shared/apis/index.ts
index 9075968..ef70adf 100644
--- a/packages/client/src/shared/apis/index.ts
+++ b/packages/client/src/shared/apis/index.ts
@@ -1,4 +1,4 @@
-export * from './AxiosInterceptor';
+export * from './core/AxiosInterceptor';
export * from './signUp';
export * from './login';
export * from './galaxy';
diff --git a/packages/client/src/shared/apis/login.ts b/packages/client/src/shared/apis/login.ts
index 825c52d..22be4fe 100644
--- a/packages/client/src/shared/apis/login.ts
+++ b/packages/client/src/shared/apis/login.ts
@@ -1,36 +1,21 @@
-import axios, { AxiosError } from 'axios';
-import Cookies from 'js-cookie';
-import { NavigateFunction } from 'react-router-dom';
-import instance from './AxiosInterceptor';
+import instance from './core/AxiosInterceptor';
-axios.defaults.withCredentials = true;
+interface PostLoginTypes {
+ username: string;
+ password: string;
+}
-export const postLogin = async (
- data: {
- username: string;
- password: string;
- },
- setIdState: React.Dispatch>,
- setPasswordState: React.Dispatch>,
- navigate: NavigateFunction,
- setIsSwitching: (value: boolean) => void,
-) => {
- try {
- await instance({
- method: 'POST',
- url: '/auth/signin',
- data,
- });
- Cookies.set('userId', data.username, { path: '/', expires: 7 });
- navigate('/home');
- setIsSwitching(true);
- } catch (err) {
- if (err instanceof AxiosError) {
- if (err.response?.status === 404) setIdState(false);
- else if (err.response?.status === 401) setPasswordState(false);
- else alert(err);
- } else alert(err);
- }
+export const postLogin = async ({ username, password }: PostLoginTypes) => {
+ const { data } = await instance({
+ method: 'POST',
+ url: '/auth/signin',
+ data: {
+ username,
+ password,
+ },
+ });
+
+ return data;
};
export const getSignInInfo = async () => {
diff --git a/packages/client/src/shared/apis/search.ts b/packages/client/src/shared/apis/search.ts
index acc673e..d7da39d 100644
--- a/packages/client/src/shared/apis/search.ts
+++ b/packages/client/src/shared/apis/search.ts
@@ -1,4 +1,4 @@
-import instance from './AxiosInterceptor';
+import instance from './core/AxiosInterceptor';
export const getNickNames = async (nickName: string) => {
const { data } = await instance({
diff --git a/packages/client/src/shared/apis/share.ts b/packages/client/src/shared/apis/share.ts
index 53a84df..547434f 100644
--- a/packages/client/src/shared/apis/share.ts
+++ b/packages/client/src/shared/apis/share.ts
@@ -1,4 +1,4 @@
-import instance from './AxiosInterceptor';
+import instance from './core/AxiosInterceptor';
export const getShareLink = async (nickName: string) => {
const { data } = await instance({
@@ -20,3 +20,12 @@ export const patchShareStatus = async (status: 'private' | 'public') => {
return data;
};
+
+export const getShareLinkHostNickName = async (shareLink: string) => {
+ const { data } = await instance({
+ method: 'GET',
+ url: `/auth/shareLink/${shareLink}`,
+ });
+
+ return data;
+};
diff --git a/packages/client/src/shared/apis/signUp.ts b/packages/client/src/shared/apis/signUp.ts
index 3817a47..5cd20d7 100644
--- a/packages/client/src/shared/apis/signUp.ts
+++ b/packages/client/src/shared/apis/signUp.ts
@@ -1,4 +1,4 @@
-import instance from './AxiosInterceptor';
+import instance from './core/AxiosInterceptor';
export const getIsAvailableUsername = async (username: string) => {
const { data } = await instance({
diff --git a/packages/client/src/shared/apis/star.ts b/packages/client/src/shared/apis/star.ts
index 4f32316..f1b9596 100644
--- a/packages/client/src/shared/apis/star.ts
+++ b/packages/client/src/shared/apis/star.ts
@@ -1,4 +1,4 @@
-import instance from './AxiosInterceptor';
+import instance from './core/AxiosInterceptor';
export const getPostListByNickName = async (nickName: string) => {
const { data } = await instance({
diff --git a/packages/client/src/shared/hooks/index.ts b/packages/client/src/shared/hooks/index.ts
index 80fe8e2..3093c6f 100644
--- a/packages/client/src/shared/hooks/index.ts
+++ b/packages/client/src/shared/hooks/index.ts
@@ -1,3 +1,2 @@
export * from './useFetch';
export * from './useForwardRef';
-export * from './useCheckLogin';
diff --git a/packages/client/src/shared/hooks/useCheckLogin.ts b/packages/client/src/shared/hooks/useCheckLogin.ts
deleted file mode 100644
index 078c7e7..0000000
--- a/packages/client/src/shared/hooks/useCheckLogin.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import instance from 'shared/apis/AxiosInterceptor';
-
-export const useCheckLogin = () => {
- const navigate = useNavigate();
- useEffect(() => {
- const checkLogin = async () => {
- try {
- const res = await instance({
- method: 'GET',
- url: '/auth/check-signin',
- });
- if (res.status === 200) {
- navigate('/home');
- }
- } catch (error) {
- console.error(error);
- }
- };
- checkLogin();
- }, []);
-};
diff --git a/packages/client/src/shared/hooks/useCheckNickName.ts b/packages/client/src/shared/hooks/useCheckNickName.ts
new file mode 100644
index 0000000..d41eb1b
--- /dev/null
+++ b/packages/client/src/shared/hooks/useCheckNickName.ts
@@ -0,0 +1,46 @@
+import { useEffect, useState } from 'react';
+import { useLocation } from 'react-router-dom';
+import { getSignInInfo } from 'shared/apis';
+import { getShareLinkHostNickName } from 'shared/apis/share';
+
+export default function useCheckNickName() {
+ const location = useLocation();
+ const [page, setPage] = useState('');
+ const [nickName, setNickName] = useState('');
+ const [status, setStatus] = useState('');
+ const [owner, setOwner] = useState('');
+
+ useEffect(() => {
+ const path = location.pathname.split('/')[1];
+ const hostNickName = location.pathname.split('/')[2];
+
+ switch (path) {
+ case 'home':
+ setPage('home');
+ (async () => {
+ const res = await getSignInInfo();
+ setNickName(res.nickname);
+ setStatus(res.status);
+ setOwner(res.nickname);
+ })();
+ break;
+
+ case 'search':
+ setPage('search');
+ setNickName(hostNickName);
+ break;
+
+ case 'guest':
+ setPage('guest');
+ (async () => {
+ const res = await getShareLinkHostNickName(hostNickName);
+ setNickName(res);
+ })();
+ break;
+ default:
+ break;
+ }
+ }, [location]);
+
+ return { page, nickName, status, owner };
+}
diff --git a/packages/client/src/shared/hooks/useRefresh.ts b/packages/client/src/shared/hooks/useRefresh.ts
new file mode 100644
index 0000000..98c1afc
--- /dev/null
+++ b/packages/client/src/shared/hooks/useRefresh.ts
@@ -0,0 +1,10 @@
+import { useEffect } from 'react';
+import { useViewStore } from 'shared/store';
+import { view } from 'shared/lib/types/view';
+
+export const useRefresh = (view: view) => {
+ const { setView } = useViewStore();
+ useEffect(() => {
+ setView(view);
+ }, []);
+};
diff --git a/packages/client/src/shared/lib/constants/camera.ts b/packages/client/src/shared/lib/constants/camera.ts
index 5e1b0e6..1451042 100644
--- a/packages/client/src/shared/lib/constants/camera.ts
+++ b/packages/client/src/shared/lib/constants/camera.ts
@@ -1,6 +1,6 @@
type Vector3 = [number, number, number];
-export const CAMERA_POSITION: Vector3 = [5000, 5000, 5000];
+export const CAMERA_POSITION: Vector3 = [10000, 10000, 8000];
export const CAMERA_FAR = 500000;
export const CAMERA_MIN_DISTANCE = 1000;
export const CAMERA_MAX_DISTANCE = 40000;
diff --git a/packages/client/src/shared/lib/constants/error.ts b/packages/client/src/shared/lib/constants/error.ts
new file mode 100644
index 0000000..175ef10
--- /dev/null
+++ b/packages/client/src/shared/lib/constants/error.ts
@@ -0,0 +1,43 @@
+interface ErrorMessageTypes {
+ get: { [key: string]: string };
+ post: { [key: string]: string };
+ patch: { [key: string]: string };
+ delete: { [key: string]: string };
+}
+
+export const errorMessage: ErrorMessageTypes = {
+ get: {
+ '/auth/check-signin': '로그인이 필요합니다.',
+ '/auth/signout': '로그아웃에 실패했습니다.',
+ '/auth/is-available-username': '이미 존재하는 아이디입니다.',
+ '/auth/is-available-nickname': '이미 존재하는 닉네임입니다.',
+ '/auth/search': '유저 목록 조회에 실패했습니다.',
+ '/auth/sharelink': '공유 링크 생성에 실패했습니다.',
+ '/post/[0-9]+': '글 조회에 실패했습니다.',
+ '/post/[0-9]+/is-liked': '좋아요 여부 조회에 실패했습니다.',
+ '/star': '글 목록 조회에 실패했습니다.',
+ '/star/by-author': '글 목록 조회에 실패했습니다.',
+ '/galaxy': '은하 형태 불러오기에 실패했습니다.',
+ '/galaxy/by-nickname': '은하 형태 불러오기에 실패했습니다.',
+ },
+
+ post: {
+ '/auth/signup': '회원가입에 실패했습니다.',
+ '/auth/signin': '아이디 또는 비밀번호가 일치하지 않습니다.',
+ '/auth/[a-zA-Z]+/signup': '회원가입에 실패했습니다.',
+ '/post': '글 작성에 실패했습니다.',
+ },
+
+ patch: {
+ '/auth/status': '검색 허용 상태 변경에 실패했습니다.',
+ '/post/[0-9]+': '글 수정에 실패했습니다.',
+ '/post/[0-9]+/like': '좋아요에 실패했습니다.',
+ '/post/[0-9]+/unlike': '좋아요 취소에 실패했습니다.',
+ '/star/[0-9]+': '별 커스텀 수정에 실패했습니다.',
+ '/galaxy': '은하 형태 변경에 실패했습니다.',
+ },
+
+ delete: {
+ '/post/[0-9]+': '글 삭제에 실패했습니다.',
+ },
+};
diff --git a/packages/client/src/shared/lib/constants/width.ts b/packages/client/src/shared/lib/constants/width.ts
index b6a62d3..8c602a9 100644
--- a/packages/client/src/shared/lib/constants/width.ts
+++ b/packages/client/src/shared/lib/constants/width.ts
@@ -1,2 +1,2 @@
export const MAX_WIDTH1 = 1210;
-export const MAX_WIDTH2 = 930;
+export const MAX_WIDTH2 = 810;
diff --git a/packages/client/src/shared/lib/types/view.ts b/packages/client/src/shared/lib/types/view.ts
new file mode 100644
index 0000000..9e91b21
--- /dev/null
+++ b/packages/client/src/shared/lib/types/view.ts
@@ -0,0 +1 @@
+export type view = 'MAIN' | 'DETAIL' | 'POST' | 'WRITING' | 'CUSTOM' | 'SHARE';
diff --git a/packages/client/src/shared/routes/PrivateRoute.tsx b/packages/client/src/shared/routes/PrivateRoute.tsx
new file mode 100644
index 0000000..486b7a8
--- /dev/null
+++ b/packages/client/src/shared/routes/PrivateRoute.tsx
@@ -0,0 +1,25 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { getSignInInfo } from 'shared/apis';
+import { useEffect, useState } from 'react';
+
+export default function PrivateRoute() {
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ await getSignInInfo();
+ setIsAuthenticated(true);
+ } catch (error) {
+ setIsAuthenticated(false);
+ } finally {
+ setIsLoading(false);
+ }
+ })();
+ }, []);
+
+ if (isLoading) return null;
+
+ return isAuthenticated && !isLoading ? : ;
+}
diff --git a/packages/client/src/shared/routes/PublicRoute.tsx b/packages/client/src/shared/routes/PublicRoute.tsx
new file mode 100644
index 0000000..0c0a21a
--- /dev/null
+++ b/packages/client/src/shared/routes/PublicRoute.tsx
@@ -0,0 +1,25 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { getSignInInfo } from 'shared/apis';
+import { useEffect, useState } from 'react';
+
+export default function PublicRoute() {
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ await getSignInInfo();
+ setIsAuthenticated(true);
+ } catch (error) {
+ setIsAuthenticated(false);
+ } finally {
+ setIsLoading(false);
+ }
+ })();
+ }, []);
+
+ if (isLoading) return null;
+
+ return isAuthenticated && !isLoading ? : ;
+}
diff --git a/packages/client/src/shared/store/useAudioStore.ts b/packages/client/src/shared/store/useAudioStore.ts
index 77bda17..613c8eb 100644
--- a/packages/client/src/shared/store/useAudioStore.ts
+++ b/packages/client/src/shared/store/useAudioStore.ts
@@ -6,6 +6,6 @@ interface PlayingState {
}
export const usePlayingStore = create((set, get) => ({
- playing: true,
+ playing: false,
setPlaying: () => set({ playing: !get().playing }),
}));
diff --git a/packages/client/src/shared/store/useOwnerStore.ts b/packages/client/src/shared/store/useOwnerStore.ts
deleted file mode 100644
index e2205a3..0000000
--- a/packages/client/src/shared/store/useOwnerStore.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { create } from 'zustand';
-
-interface OwnerState {
- isMyPage: boolean;
- pageOwnerNickName: string;
- setIsMyPage: (value: boolean) => void;
- setPageOwnerNickName: (value: string) => void;
-}
-
-export const useOwnerStore = create((set) => ({
- isMyPage: true,
- pageOwnerNickName: '',
- setIsMyPage: (value) => {
- if (value === true) set((state) => ({ ...state, pageOwnerNickName: '' }));
- set((state) => ({ ...state, isMyPage: value }));
- },
- setPageOwnerNickName: (value) => {
- set((state) => ({ ...state, pageOwnerNickName: value }));
- },
-}));
diff --git a/packages/client/src/shared/store/useScreenSwitchStore.ts b/packages/client/src/shared/store/useScreenSwitchStore.ts
deleted file mode 100644
index 438c88f..0000000
--- a/packages/client/src/shared/store/useScreenSwitchStore.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { create } from 'zustand';
-
-interface ScreenSwitchState {
- isSwitching: boolean;
- setIsSwitching: (value: boolean) => void;
-}
-
-export const useScreenSwitchStore = create((set) => ({
- isSwitching: false,
- setIsSwitching: (value) => {
- set((state) => ({ ...state, isSwitching: value }));
- },
-}));
diff --git a/packages/client/src/shared/store/useToastStore.ts b/packages/client/src/shared/store/useToastStore.ts
index 2dbcf1e..4dd192e 100644
--- a/packages/client/src/shared/store/useToastStore.ts
+++ b/packages/client/src/shared/store/useToastStore.ts
@@ -2,13 +2,21 @@ import { create } from 'zustand';
interface ToastState {
text: string;
- setText: (text: string) => void;
+ type: 'success' | 'error';
+ setToast: ({
+ text,
+ type,
+ }: {
+ text: string;
+ type: 'success' | 'error';
+ }) => void;
}
export const useToastStore = create((set) => ({
text: '',
- setText: (text) => {
- set((state) => ({ ...state, text }));
+ type: 'success',
+ setToast: ({ text, type }) => {
+ set((state) => ({ ...state, text, type }));
setTimeout(() => {
set((state) => ({ ...state, text: '' }));
diff --git a/packages/client/src/shared/store/useViewStore.ts b/packages/client/src/shared/store/useViewStore.ts
index c5ffcbd..688f565 100644
--- a/packages/client/src/shared/store/useViewStore.ts
+++ b/packages/client/src/shared/store/useViewStore.ts
@@ -1,6 +1,5 @@
import { create } from 'zustand';
-
-type view = 'MAIN' | 'DETAIL' | 'POST' | 'WRITING' | 'CUSTOM' | 'SHARE';
+import { view } from 'shared/lib/types/view';
interface ViewState {
view: view;
diff --git a/packages/client/src/shared/ui/alert/Alert.tsx b/packages/client/src/shared/ui/alert/Alert.tsx
new file mode 100644
index 0000000..c9188f7
--- /dev/null
+++ b/packages/client/src/shared/ui/alert/Alert.tsx
@@ -0,0 +1,99 @@
+import styled from '@emotion/styled';
+import { Body02ME, Title01 } from '../styles';
+import { css } from '@emotion/react';
+import { Button } from '..';
+import { useState, useEffect } from 'react';
+
+interface PropsTypes extends React.HTMLAttributes {
+ title: string;
+ description?: string;
+}
+
+export default function Alert({ title, description, ...args }: PropsTypes) {
+ const [isOpen, setIsOpen] = useState(true);
+
+ const handleConfirmButton = () => {
+ setIsOpen(false);
+ };
+
+ if (!isOpen) return null;
+
+ const handleKeyPress = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') handleConfirmButton();
+ };
+
+ useEffect(() => {
+ window.addEventListener('keydown', handleKeyPress);
+
+ return () => window.removeEventListener('keydown', handleKeyPress);
+ }, []);
+
+ return (
+
+
+ {title}
+ {description && {description}}
+
+
+
+
+
+
+ );
+}
+
+const Overlay = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 998;
+ background-color: rgba(0, 0, 0, 0.5);
+`;
+
+const Layout = styled.div`
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 999;
+
+ display: flex;
+ flex-direction: column;
+ width: 516px;
+ border-radius: 16px;
+ padding: 32px;
+ margin: 12px 0 0 0;
+
+ ${({ theme: { colors } }) => css`
+ background-color: ${colors.background.bdp01_80};
+ border: 2px solid ${colors.warning.filled};
+ `};
+`;
+
+const Title = styled.h1`
+ display: flex;
+ justify-content: flex-start;
+ margin: 0 0 8px 0;
+ color: ${({ theme: { colors } }) => colors.text.primary};
+ ${Title01}
+`;
+
+const Description = styled.p`
+ color: ${({ theme: { colors } }) => colors.text.third};
+ ${Body02ME}
+`;
+
+const ButtonWrapper = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ margin: 32px 0 0 0;
+`;
diff --git a/packages/client/src/shared/ui/alert/index.ts b/packages/client/src/shared/ui/alert/index.ts
new file mode 100644
index 0000000..79e3b15
--- /dev/null
+++ b/packages/client/src/shared/ui/alert/index.ts
@@ -0,0 +1 @@
+export * from './Alert';
diff --git a/packages/client/src/shared/ui/alertDialog/AlertDialog.tsx b/packages/client/src/shared/ui/alertDialog/AlertDialog.tsx
index 9a8a096..2ed1b0f 100644
--- a/packages/client/src/shared/ui/alertDialog/AlertDialog.tsx
+++ b/packages/client/src/shared/ui/alertDialog/AlertDialog.tsx
@@ -10,6 +10,7 @@ interface PropsTypes extends React.HTMLAttributes {
onClickCancelButton: () => void;
onClickActionButton: () => void;
description?: string;
+ disabled: boolean;
}
export default function AlertDialog({
@@ -19,6 +20,7 @@ export default function AlertDialog({
actionButtonText,
onClickCancelButton,
onClickActionButton,
+ disabled,
...args
}: PropsTypes) {
return (
@@ -41,6 +43,7 @@ export default function AlertDialog({
size="m"
type="button"
onClick={onClickActionButton}
+ disabled={disabled}
>
{actionButtonText}
@@ -56,7 +59,7 @@ const Overlay = styled.div`
left: 0;
width: 100%;
height: 100%;
- z-index: 998;
+ z-index: 1001;
background-color: rgba(0, 0, 0, 0.5);
`;
@@ -65,7 +68,7 @@ const Layout = styled.div`
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
- z-index: 999;
+ z-index: 1002;
display: flex;
flex-direction: column;
diff --git a/packages/client/src/shared/ui/alertDialog/index.ts b/packages/client/src/shared/ui/alertDialog/index.ts
new file mode 100644
index 0000000..5c2d628
--- /dev/null
+++ b/packages/client/src/shared/ui/alertDialog/index.ts
@@ -0,0 +1 @@
+export { default as AlertDialog } from './AlertDialog';
diff --git a/packages/client/src/shared/ui/audioButton/AudioButton.tsx b/packages/client/src/shared/ui/audioButton/AudioButton.tsx
new file mode 100644
index 0000000..b6cf0db
--- /dev/null
+++ b/packages/client/src/shared/ui/audioButton/AudioButton.tsx
@@ -0,0 +1,29 @@
+import { Volume2, VolumeX } from 'lucide-react';
+import { usePlayingStore } from 'shared/store/useAudioStore';
+import styled from '@emotion/styled';
+
+export default function AudioButton() {
+ const { playing, setPlaying } = usePlayingStore();
+
+ return (
+
+ {playing ? : }
+
+ );
+}
+
+const Wrapper = styled.div`
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ z-index: 100;
+ cursor: pointer;
+`;
+
+const MuteIcon = styled(VolumeX)`
+ color: ${({ theme }) => theme.colors.text.secondary};
+`;
+
+const UnMuteIcon = styled(Volume2)`
+ color: ${({ theme }) => theme.colors.text.secondary};
+`;
diff --git a/packages/client/src/shared/ui/buttons/Button.tsx b/packages/client/src/shared/ui/buttons/Button.tsx
index d9fd508..3fe7d82 100644
--- a/packages/client/src/shared/ui/buttons/Button.tsx
+++ b/packages/client/src/shared/ui/buttons/Button.tsx
@@ -64,6 +64,7 @@ const CustomButton = styled.button`
border-color: transparent;
background: ${colors.primary.disabled};
color: ${colors.text.disabled};
+ box-shadow: none;
}
`;
case 'warning':
@@ -76,6 +77,12 @@ const CustomButton = styled.button`
&:hover {
background: ${colors.warning.filled};
}
+ &:disabled {
+ border-color: transparent;
+ background: ${colors.primary.disabled};
+ color: ${colors.text.disabled};
+ box-shadow: none;
+ }
`;
case 'warning-border':
return css`
@@ -87,6 +94,12 @@ const CustomButton = styled.button`
&:hover {
background: ${colors.warning.filled_10};
}
+ &:disabled {
+ border-color: transparent;
+ background: ${colors.warning.filled_10};
+ color: ${colors.text.disabled};
+ box-shadow: none;
+ }
`;
}
}}
diff --git a/packages/client/src/shared/ui/index.ts b/packages/client/src/shared/ui/index.ts
index 1c58276..7f5edc9 100644
--- a/packages/client/src/shared/ui/index.ts
+++ b/packages/client/src/shared/ui/index.ts
@@ -5,3 +5,4 @@ export * from './search';
export * from './textArea';
export * from './toast';
export * from './slider';
+export * from './alertDialog';
diff --git a/packages/client/src/shared/ui/search/Search.tsx b/packages/client/src/shared/ui/search/Search.tsx
index f95e934..656b3d3 100644
--- a/packages/client/src/shared/ui/search/Search.tsx
+++ b/packages/client/src/shared/ui/search/Search.tsx
@@ -9,6 +9,7 @@ interface PropsTypes {
results?: string[];
inputState: string;
setInputState: React.Dispatch>;
+ disabled: boolean;
}
export default function Search({
@@ -17,6 +18,7 @@ export default function Search({
setInputState,
placeholder = '',
results = [],
+ disabled,
...args
}: PropsTypes) {
const onChangeSearchInput = ({
@@ -52,7 +54,7 @@ export default function Search({
size="m"
buttonType="CTA-icon"
type="submit"
- disabled={!inputState}
+ disabled={!inputState || disabled}
>
검색
diff --git a/packages/client/src/shared/ui/textArea/TextArea.tsx b/packages/client/src/shared/ui/textArea/TextArea.tsx
index c13cf98..d5f87df 100644
--- a/packages/client/src/shared/ui/textArea/TextArea.tsx
+++ b/packages/client/src/shared/ui/textArea/TextArea.tsx
@@ -150,6 +150,24 @@ const Wrapper = styled.div<{ css: SerializedStyles }>`
margin-top: 9px;
margin-right: 9px;
color: ${theme.colors.text.secondary};
+
+ & ol {
+ padding-left: 40px;
+ margin: 18px 0;
+ }
+
+ & ul {
+ padding-left: 40px;
+ margin: 18px 0;
+ }
+
+ & ol li {
+ list-style: decimal;
+ }
+
+ & ul li {
+ list-style: disc;
+ }
`;
const scrollStyle = css`
diff --git a/packages/client/src/shared/ui/toast/Toast.tsx b/packages/client/src/shared/ui/toast/Toast.tsx
index a819782..1312586 100644
--- a/packages/client/src/shared/ui/toast/Toast.tsx
+++ b/packages/client/src/shared/ui/toast/Toast.tsx
@@ -3,12 +3,14 @@ import { Body04BD } from '../styles';
import confirmIcon from '@icons/icon-confirm-22.svg';
import { useState } from 'react';
import { keyframes } from '@emotion/react';
+import { X } from 'lucide-react';
interface PropsTypes {
+ type?: 'success' | 'error';
children: string;
}
-export default function Toast({ children }: PropsTypes) {
+export default function Toast({ type = 'success', children }: PropsTypes) {
const [visible, setVisible] = useState(true);
const handleAnimationEnd = () => setVisible(false);
@@ -17,7 +19,12 @@ export default function Toast({ children }: PropsTypes) {
return (
-
+ {type === 'success' ? (
+
+ ) : (
+
+ )}
+
{children}
);
@@ -38,6 +45,7 @@ const Layout = styled.div`
padding: 16px 24px;
border-radius: 40px;
background-color: ${({ theme }) => theme.colors.primary.filled};
+ align-items: center;
animation: ${fadeOutAnimation} 1s ease forwards;
animation-delay: 2s;
diff --git a/packages/client/src/widgets/error/FallBackComponent.tsx b/packages/client/src/widgets/error/FallBackComponent.tsx
new file mode 100644
index 0000000..c6fc5f5
--- /dev/null
+++ b/packages/client/src/widgets/error/FallBackComponent.tsx
@@ -0,0 +1,55 @@
+import styled from '@emotion/styled';
+import Logo from 'assets/logos/404.png';
+import { useNavigate } from 'react-router-dom';
+import { Body05Me, PageTitle03 } from 'shared/ui/styles';
+
+export default function FallBackComponent() {
+ const navigate = useNavigate();
+ return (
+
+
+ 404
+ 페이지를 찾을 수 없습니다.
+
+
+ );
+}
+
+const Layout = styled.div`
+ height: 100vh;
+ width: 100vw;
+ background-color: #070614;
+ color: ${({ theme }) => theme.colors.text.primary};
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ ${Body05Me}
+`;
+
+const LogoImage = styled.img`
+ width: 30vw;
+ margin-bottom: 80px;
+`;
+
+const Title = styled.p`
+ ${PageTitle03};
+ margin-bottom: 40px;
+`;
+
+const Button = styled.button`
+ ${Body05Me}
+ padding: 10px 20px;
+ border-radius: 4px;
+ background-color: ${({ theme }) => theme.colors.primary.filled};
+ color: ${({ theme }) => theme.colors.text.primary};
+ border: none;
+ cursor: pointer;
+`;
diff --git a/packages/client/src/widgets/galaxy/lib/constants/index.ts b/packages/client/src/widgets/galaxy/lib/constants/index.ts
index 8448e22..9763cf5 100644
--- a/packages/client/src/widgets/galaxy/lib/constants/index.ts
+++ b/packages/client/src/widgets/galaxy/lib/constants/index.ts
@@ -1,11 +1,16 @@
-export const STARS_NUM = 6000;
+export const STARS_NUM = 4000;
export const DISTANCE_LIMIT = 3000;
export const starTypes = {
- percentage: [0.7645, 0.121, 0.076, 0.03, 0.006, 0.0013],
- color: [0xffcc6f, 0xffd2a1, 0xfff4ea, 0xf8f7ff, 0xcad7ff, 0xaabfff],
- size: [0.7, 0.7, 1.15, 1.48, 2.0, 2.5, 3.5],
+ percentage: [
+ 0.07, 0.1, 0.02, 0.14, 0.14, 0.07, 0.07, 0.02, 0.2, 0.2, 0.2, 0.2,
+ ],
+ color: [
+ 0xffcece, 0xffe8ce, 0xceffe6, 0xcef9ff, 0xd2ceff, 0xff9d9d, 0xfffa9d,
+ 0xb9ff9d, 0x9db9ff, 0xca9dff, 0x6445ff, 0x4570ff,
+ ],
+ size: [0.5, 0.5, 0.3, 0.8, 0.3, 0.5, 0.5, 0.3, 1.3, 1.3, 1.1, 1.1],
};
export const ARMS_X_DIST = 5000;
@@ -13,4 +18,4 @@ export const ARMS_Z_DIST = 1000;
export const GALAXY_THICKNESS = 300;
export const SPIRAL = 1.2;
export const SPIRAL_START = 1000;
-export const STARS_DENSITY = 0.5;
+export const STARS_DENSITY = 0.4;
diff --git a/packages/client/src/widgets/galaxy/lib/modules/positions.ts b/packages/client/src/widgets/galaxy/lib/modules/positions.ts
index 45f88d6..d87c029 100644
--- a/packages/client/src/widgets/galaxy/lib/modules/positions.ts
+++ b/packages/client/src/widgets/galaxy/lib/modules/positions.ts
@@ -11,7 +11,7 @@ interface GalaxyInfo {
}
export const getSpiralPositions = () => {
- const x = getGaussianRandomFloat(0, ARMS_X_DIST);
+ const x = getGaussianRandomFloat(0, ARMS_X_DIST * 1.5);
const y = getGaussianRandomFloat(0, 1);
const z = getGaussianRandomFloat(0, 1);
return new THREE.Vector3(x, y, z);
diff --git a/packages/client/src/widgets/galaxyCustomModal/GalaxyCustomModal.tsx b/packages/client/src/widgets/galaxyCustomModal/GalaxyCustomModal.tsx
index 05c6d53..bdf2c51 100644
--- a/packages/client/src/widgets/galaxyCustomModal/GalaxyCustomModal.tsx
+++ b/packages/client/src/widgets/galaxyCustomModal/GalaxyCustomModal.tsx
@@ -7,18 +7,28 @@ import {
LeftButton,
TopButton,
} from './ui';
-import { useViewStore } from 'shared/store';
+import { useToastStore, useViewStore } from 'shared/store';
import styled from '@emotion/styled';
import { useGalaxyStore, useCustomStore } from 'shared/store';
import { postGalaxy } from 'shared/apis';
+import { useRefresh } from 'shared/hooks/useRefresh';
+import { useState } from 'react';
+import AlertDialog from 'shared/ui/alertDialog/AlertDialog';
export default function GalaxyCustomModal() {
const navigate = useNavigate();
const { setView } = useViewStore();
const galaxy = useGalaxyStore();
const { spiral, start, thickness, zDist } = useCustomStore();
+ const [isSubmitButtonDisabled, setIsSubmitButtonDisabled] = useState(false);
+ const { setToast } = useToastStore();
+ const [dialog, setDialog] = useState(false);
- const handleSubmit = () => {
+ useRefresh('CUSTOM');
+
+ const handleSubmit = async () => {
+ if (isSubmitButtonDisabled) return;
+ setIsSubmitButtonDisabled(true);
const galaxyStyle = {
spiral: galaxy.spiral !== spiral ? spiral : undefined,
start: galaxy.start !== start ? start : undefined,
@@ -30,14 +40,17 @@ export default function GalaxyCustomModal() {
galaxy.setStart(start);
galaxy.setThickness(thickness);
galaxy.setZDist(zDist);
- postGalaxy(galaxyStyle);
+ await postGalaxy(galaxyStyle);
};
return (
);
diff --git a/packages/client/src/widgets/galaxyCustomModal/ui/RightButton.tsx b/packages/client/src/widgets/galaxyCustomModal/ui/RightButton.tsx
index b8ecc9b..3e72dc3 100644
--- a/packages/client/src/widgets/galaxyCustomModal/ui/RightButton.tsx
+++ b/packages/client/src/widgets/galaxyCustomModal/ui/RightButton.tsx
@@ -1,8 +1,12 @@
import { Button } from 'shared/ui';
-export default function RightButton() {
+interface PropsType {
+ disabled: boolean;
+}
+
+export default function RightButton({ disabled }: PropsType) {
return (
-
);
diff --git a/packages/client/src/widgets/landingScreen/LandingScreen.tsx b/packages/client/src/widgets/landingScreen/LandingScreen.tsx
index 6fa339b..25290ad 100644
--- a/packages/client/src/widgets/landingScreen/LandingScreen.tsx
+++ b/packages/client/src/widgets/landingScreen/LandingScreen.tsx
@@ -19,7 +19,7 @@ export default function LandingScreen({
return (
-