diff --git a/package-lock.json b/package-lock.json
index fc2967f..659097f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,11 +8,19 @@
"name": "diglog-react",
"version": "0.0.0",
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/modifiers": "^9.0.0",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@faker-js/faker": "^9.4.0",
"@reduxjs/toolkit": "^2.5.0",
"@tailwindcss/vite": "^4.0.0",
+ "@tinymce/tinymce-react": "^5.1.1",
"axios": "^1.7.9",
+ "dompurify": "^3.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-icons": "^5.4.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.3",
"tailwindcss": "^4.0.0"
@@ -327,6 +335,73 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@dnd-kit/accessibility": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
+ "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/core": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
+ "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@dnd-kit/accessibility": "^3.1.1",
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/modifiers": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
+ "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@dnd-kit/core": "^6.3.0",
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/sortable": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
+ "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@dnd-kit/core": "^6.3.0",
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/utilities": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
+ "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
@@ -868,6 +943,22 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@faker-js/faker": {
+ "version": "9.4.0",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.4.0.tgz",
+ "integrity": "sha512-85+k0AxaZSTowL0gXp8zYWDIrWclTbRPg/pm/V0dSFZ6W6D4lhcG3uuZl4zLsEKfEvs69xDbLN2cHQudwp95JA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fakerjs"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=9.0.0"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1520,6 +1611,20 @@
"vite": "^5.2.0 || ^6"
}
},
+ "node_modules/@tinymce/tinymce-react": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-5.1.1.tgz",
+ "integrity": "sha512-DQ0wpvnf/9z8RsOEAmrWZ1DN1PKqcQHfU+DpM3llLze7FHmxVtzuN8O+FYh0oAAF4stzAXwiCIVacfqjMwRieQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.6.2",
+ "tinymce": "^7.0.0 || ^6.0.0 || ^5.5.1"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ^17.0.1 || ^16.7.0",
+ "react-dom": "^18.0.0 || ^17.0.1 || ^16.7.0"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1612,6 +1717,13 @@
"@types/react": "^18.0.0"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -2172,6 +2284,15 @@
"node": ">=0.10"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
+ "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.86",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.86.tgz",
@@ -3220,6 +3341,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3360,6 +3490,17 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3422,6 +3563,21 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-icons": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
+ "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@@ -3697,6 +3853,12 @@
"node": ">=6"
}
},
+ "node_modules/tinymce": {
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.6.1.tgz",
+ "integrity": "sha512-5cHhaAoyyTHfAVTInNfoSp0KkUHmeVUbGSu37QKQbOFIPqxYPhqBiaLm1WVLgoNBYOHRProVc3xzxnNTeWHyoQ==",
+ "license": "GPL-2.0-or-later"
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3723,6 +3885,12 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
diff --git a/package.json b/package.json
index 04bae5f..f9c565d 100644
--- a/package.json
+++ b/package.json
@@ -10,11 +10,19 @@
"preview": "vite preview"
},
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/modifiers": "^9.0.0",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@faker-js/faker": "^9.4.0",
"@reduxjs/toolkit": "^2.5.0",
"@tailwindcss/vite": "^4.0.0",
+ "@tinymce/tinymce-react": "^5.1.1",
"axios": "^1.7.9",
+ "dompurify": "^3.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-icons": "^5.4.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.3",
"tailwindcss": "^4.0.0"
diff --git a/public/kakao-logo.png b/public/kakao-logo.png
new file mode 100644
index 0000000..a782468
Binary files /dev/null and b/public/kakao-logo.png differ
diff --git a/public/kakao_login_medium_wide.png b/public/kakao_login_medium_wide.png
deleted file mode 100644
index c882acc..0000000
Binary files a/public/kakao_login_medium_wide.png and /dev/null differ
diff --git a/public/logo-black.png b/public/logo-black.png
deleted file mode 100644
index b9dcf3a..0000000
Binary files a/public/logo-black.png and /dev/null differ
diff --git a/public/logo.png b/public/logo.png
new file mode 100644
index 0000000..e4809f5
Binary files /dev/null and b/public/logo.png differ
diff --git a/src/App.tsx b/src/App.tsx
index ab59ad6..06da3e8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,5 @@
import {RouterProvider} from 'react-router-dom'
-import root from './router/root'
+import root from './common/router/root'
function App() {
diff --git a/src/apis/AxiosApi.tsx b/src/common/apis/AxiosApi.tsx
similarity index 97%
rename from src/apis/AxiosApi.tsx
rename to src/common/apis/AxiosApi.tsx
index 442dca6..f654483 100644
--- a/src/apis/AxiosApi.tsx
+++ b/src/common/apis/AxiosApi.tsx
@@ -1,5 +1,5 @@
import axios from "axios";
-import store from "../store";
+import store from "../../store.tsx";
import {login} from "../slices/loginSlice.tsx";
const axiosApi = axios.create({
diff --git a/src/apis/member.tsx b/src/common/apis/member.tsx
similarity index 100%
rename from src/apis/member.tsx
rename to src/common/apis/member.tsx
diff --git a/src/common/assets/fonts/Jalnan2.otf b/src/common/assets/fonts/Jalnan2.otf
new file mode 100644
index 0000000..1e3ef1d
Binary files /dev/null and b/src/common/assets/fonts/Jalnan2.otf differ
diff --git a/src/common/assets/fonts/s-core/SCDream1.otf b/src/common/assets/fonts/s-core/SCDream1.otf
new file mode 100644
index 0000000..c8eac97
Binary files /dev/null and b/src/common/assets/fonts/s-core/SCDream1.otf differ
diff --git a/src/common/assets/fonts/s-core/SCDream2.otf b/src/common/assets/fonts/s-core/SCDream2.otf
new file mode 100644
index 0000000..c5625d9
Binary files /dev/null and b/src/common/assets/fonts/s-core/SCDream2.otf differ
diff --git a/src/common/assets/fonts/s-core/SCDream3.otf b/src/common/assets/fonts/s-core/SCDream3.otf
new file mode 100644
index 0000000..28a160c
Binary files /dev/null and b/src/common/assets/fonts/s-core/SCDream3.otf differ
diff --git a/src/common/assets/fonts/s-core/SCDream4.otf b/src/common/assets/fonts/s-core/SCDream4.otf
new file mode 100644
index 0000000..1978ea4
Binary files /dev/null and b/src/common/assets/fonts/s-core/SCDream4.otf differ
diff --git a/src/common/assets/fonts/s-core/SCDream5.otf b/src/common/assets/fonts/s-core/SCDream5.otf
new file mode 100644
index 0000000..628e828
Binary files /dev/null and b/src/common/assets/fonts/s-core/SCDream5.otf differ
diff --git a/src/common/assets/fonts/s-core/SCDream6.otf b/src/common/assets/fonts/s-core/SCDream6.otf
new file mode 100644
index 0000000..4e20297
Binary files /dev/null and b/src/common/assets/fonts/s-core/SCDream6.otf differ
diff --git a/src/common/assets/fonts/s-core/SCDream7.otf b/src/common/assets/fonts/s-core/SCDream7.otf
new file mode 100644
index 0000000..7e38170
Binary files /dev/null and b/src/common/assets/fonts/s-core/SCDream7.otf differ
diff --git a/src/common/assets/fonts/s-core/SCDream8.otf b/src/common/assets/fonts/s-core/SCDream8.otf
new file mode 100644
index 0000000..5d7e4be
Binary files /dev/null and b/src/common/assets/fonts/s-core/SCDream8.otf differ
diff --git a/src/common/assets/fonts/s-core/SCDream9.otf b/src/common/assets/fonts/s-core/SCDream9.otf
new file mode 100644
index 0000000..f6db18f
Binary files /dev/null and b/src/common/assets/fonts/s-core/SCDream9.otf differ
diff --git "a/src/common/assets/fonts/s-core/\354\227\220\354\212\244\354\275\224\354\226\264 \353\223\234\353\246\274_\353\235\274\354\235\264\354\204\240\354\212\244.png" "b/src/common/assets/fonts/s-core/\354\227\220\354\212\244\354\275\224\354\226\264 \353\223\234\353\246\274_\353\235\274\354\235\264\354\204\240\354\212\244.png"
new file mode 100644
index 0000000..d06ffa0
Binary files /dev/null and "b/src/common/assets/fonts/s-core/\354\227\220\354\212\244\354\275\224\354\226\264 \353\223\234\353\246\274_\353\235\274\354\235\264\354\204\240\354\212\244.png" differ
diff --git a/src/common/router/blogRouter.tsx b/src/common/router/blogRouter.tsx
new file mode 100644
index 0000000..c5b6a3c
--- /dev/null
+++ b/src/common/router/blogRouter.tsx
@@ -0,0 +1,14 @@
+import {Suspense} from "react";
+import {Blog, Loading} from "./page.tsx";
+
+const blogRouter = () => {
+
+ return [
+ {
+ path: ':username',
+ element:
+ },
+ ];
+}
+
+export default blogRouter;
\ No newline at end of file
diff --git a/src/router/page.tsx b/src/common/router/page.tsx
similarity index 63%
rename from src/router/page.tsx
rename to src/common/router/page.tsx
index 6379c43..63bcab4 100644
--- a/src/router/page.tsx
+++ b/src/common/router/page.tsx
@@ -14,5 +14,17 @@ export const Loading =
;
-export const Main = lazy(() => import('../pages/MainPage'));
-export const Login = lazy(() => import('../pages/member/LoginPage'));
\ No newline at end of file
+export const Main = lazy(() => import('../../pages/MainPage.tsx'));
+export const Login = lazy(() => import('../../pages/member/LoginPage.tsx'));
+export const Platform = lazy(() => import('../../pages/member/PlatformPage.tsx'));
+export const Email = lazy(() => import('../../pages/member/EmailPage.tsx'));
+export const Code = lazy(() => import('../../pages/member/CodePage.tsx'));
+export const Signup = lazy(() => import('../../pages/member/SignupPage.tsx'));
+export const Setting = lazy(() => import('../../pages/setting/SettingPage.tsx'));
+
+export const Search = lazy(() => import('../../pages/post/SearchPage.tsx'));
+
+export const Post = lazy(() => import('../../pages/post/PostPage.tsx'));
+export const Write = lazy(() => import('../../pages/post/WritePage.tsx'));
+
+export const Blog = lazy(() => import('../../pages/blog/BlogPage.tsx'));
diff --git a/src/common/router/postRouter.tsx b/src/common/router/postRouter.tsx
new file mode 100644
index 0000000..1188aa8
--- /dev/null
+++ b/src/common/router/postRouter.tsx
@@ -0,0 +1,14 @@
+import {Suspense} from "react";
+import {Loading, Post} from "./page.tsx";
+
+const postRouter = () => {
+
+ return [
+ {
+ path: ':id',
+ element:
+ },
+ ];
+}
+
+export default postRouter;
\ No newline at end of file
diff --git a/src/common/router/root.tsx b/src/common/router/root.tsx
new file mode 100644
index 0000000..467853e
--- /dev/null
+++ b/src/common/router/root.tsx
@@ -0,0 +1,43 @@
+import {createBrowserRouter} from "react-router-dom";
+import {Suspense} from "react";
+import {Loading, Login, Main, Search, Setting, Write} from "./page.tsx";
+import postRouter from "./postRouter.tsx";
+import blogRouter from "./blogRouter.tsx";
+import signupRouter from "./signupRouter.tsx";
+
+const root = createBrowserRouter([
+ {
+ path: '',
+ element:
+ },
+ {
+ path: '/login',
+ element:
+ },
+ {
+ path: '/setting',
+ element:
+ },
+ {
+ path: '/search',
+ element:
+ },
+ {
+ path: '/write',
+ element:
+ },
+ {
+ path: '/signup',
+ children: signupRouter()
+ },
+ {
+ path: '/post',
+ children: postRouter()
+ },
+ {
+ path: '/blog',
+ children: blogRouter()
+ },
+]);
+
+export default root;
\ No newline at end of file
diff --git a/src/common/router/signupRouter.tsx b/src/common/router/signupRouter.tsx
new file mode 100644
index 0000000..f8c46b6
--- /dev/null
+++ b/src/common/router/signupRouter.tsx
@@ -0,0 +1,26 @@
+import {Code, Email, Loading, Platform, Signup} from "./page.tsx";
+import {Suspense} from "react";
+
+const signupRouter = () => {
+
+ return [
+ {
+ path: 'platform',
+ element:
+ },
+ {
+ path: 'email',
+ element:
+ },
+ {
+ path: 'code',
+ element:
+ },
+ {
+ path: '',
+ element:
+ },
+ ];
+}
+
+export default signupRouter;
\ No newline at end of file
diff --git a/src/slices/loginSlice.tsx b/src/common/slices/loginSlice.tsx
similarity index 63%
rename from src/slices/loginSlice.tsx
rename to src/common/slices/loginSlice.tsx
index 7040b2e..6b84a14 100644
--- a/src/slices/loginSlice.tsx
+++ b/src/common/slices/loginSlice.tsx
@@ -1,11 +1,11 @@
import {createSlice} from "@reduxjs/toolkit";
const initialState = {
- isLogin: false,
- accessToken: "",
- email: "",
- username: "",
- roles: [],
+ isLogin: true,
+ accessToken: "token",
+ email: "diglog@example.com",
+ username: "diglog",
+ roles: ["ROLE_USER"],
}
const loginSlice = createSlice({
@@ -24,9 +24,15 @@ const loginSlice = createSlice({
logout: () => {
return initialState;
},
+ setUsername: (state, action) => {
+ return {
+ ...state,
+ username: action.payload.username,
+ };
+ },
}
});
-export const {login, logout} = loginSlice.actions;
+export const {login, logout, setUsername} = loginSlice.actions;
export default loginSlice.reducer;
\ No newline at end of file
diff --git a/src/common/types/common.tsx b/src/common/types/common.tsx
new file mode 100644
index 0000000..9f428c4
--- /dev/null
+++ b/src/common/types/common.tsx
@@ -0,0 +1,6 @@
+export interface PageResponse {
+ size: number;
+ number: number;
+ totalElements: number;
+ totalPages: number;
+}
\ No newline at end of file
diff --git a/src/common/types/post.tsx b/src/common/types/post.tsx
new file mode 100644
index 0000000..b77f423
--- /dev/null
+++ b/src/common/types/post.tsx
@@ -0,0 +1,21 @@
+import {PageResponse} from "./common.tsx";
+
+export interface PostListResponse {
+ content: PostResponse[];
+ page: PageResponse;
+}
+
+export interface PostResponse {
+ id: string;
+ title: string;
+ content: string;
+ username: string;
+ tags: TagResponse[];
+ createdAt: Date;
+}
+
+export interface TagResponse {
+ id: string;
+ name: string;
+}
+
diff --git a/src/common/util/date.tsx b/src/common/util/date.tsx
new file mode 100644
index 0000000..9980acb
--- /dev/null
+++ b/src/common/util/date.tsx
@@ -0,0 +1,27 @@
+export const fullDateToKorean = (date: Date) => {
+ const outputDate = new Date(date);
+ return outputDate.getFullYear() + '년 '
+ + (outputDate.getMonth() + 1) + '월 '
+ + outputDate.getDate() + '일 '
+ + outputDate.getHours().toString().padStart(2, '0') + ':'
+ + outputDate.getMinutes().toString().padStart(2, '0');
+}
+
+export const dateToKorean = (date: Date) => {
+ const outputDate = new Date(date);
+ return outputDate.getFullYear() + '년 '
+ + (outputDate.getMonth() + 1) + '월 '
+ + outputDate.getDate() + '일';
+}
+
+export const shortDateToKorean = (date: Date) => {
+ const outputDate = new Date(date);
+ return (outputDate.getMonth() + 1) + '월 '
+ + outputDate.getDate() + '일 ';
+}
+
+export const formatTimer = (seconds: number) => {
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds % 60;
+ return `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
+};
\ No newline at end of file
diff --git a/src/common/util/html.tsx b/src/common/util/html.tsx
new file mode 100644
index 0000000..2cec434
--- /dev/null
+++ b/src/common/util/html.tsx
@@ -0,0 +1,12 @@
+export const removeImgTags = (content: string) => {
+ const doc = new DOMParser().parseFromString(content, 'text/html');
+ const imgTags = doc.querySelectorAll('img');
+ imgTags.forEach((img) => img.remove());
+ return doc.body.innerHTML;
+}
+
+export const getImgSrc = (content: string) => {
+ const doc = new DOMParser().parseFromString(content, 'text/html');
+ const imgTag = doc.querySelector('img');
+ return imgTag ? imgTag.src : null;
+}
\ No newline at end of file
diff --git a/src/common/util/regex.tsx b/src/common/util/regex.tsx
new file mode 100644
index 0000000..fc95398
--- /dev/null
+++ b/src/common/util/regex.tsx
@@ -0,0 +1,10 @@
+export const checkEmail = (email: string) => {
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return email !== "" && email.match(regex);
+}
+
+export const checkPassword = (password: string) => {
+ const regex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d\W_]{8,16}$/;
+
+ return password !== "" && password.match(regex);
+}
\ No newline at end of file
diff --git a/src/components/common/FillButton.tsx b/src/components/common/FillButton.tsx
new file mode 100644
index 0000000..46a3652
--- /dev/null
+++ b/src/components/common/FillButton.tsx
@@ -0,0 +1,25 @@
+import {Link} from "react-router-dom";
+
+const className = " bg-lime-500 hover:bg-lime-400 text-white font-bold py-2 px-4 rounded hover:cursor-pointer";
+
+export function FillButton({text, onClick, addStyle}: { text: string, onClick: () => void, addStyle?: string }) {
+ return (
+
+ );
+}
+
+export function FillLink({text, to}: { text: string, to: string }) {
+ return (
+
+ {text}
+
+ );
+}
+
+export function LoadMoreButton({onClick, addStyle}: { onClick: () => void, addStyle?: string }) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/common/Footer.tsx b/src/components/common/Footer.tsx
new file mode 100644
index 0000000..1d3b6a8
--- /dev/null
+++ b/src/components/common/Footer.tsx
@@ -0,0 +1,15 @@
+import {FaGithub} from "react-icons/fa";
+import {Link} from "react-router-dom";
+
+function Footer() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default Footer;
\ No newline at end of file
diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx
index 27564c1..b9d9617 100644
--- a/src/components/common/Header.tsx
+++ b/src/components/common/Header.tsx
@@ -1,32 +1,110 @@
-import {Link} from "react-router-dom";
+import {Link, useNavigate} from "react-router-dom";
import {useSelector} from "react-redux";
import {RootState} from "../../store.tsx";
+import {faker} from "@faker-js/faker/locale/ko";
+import {useEffect, useRef, useState} from "react";
+import {TextLink} from "./TextButton.tsx";
+import {MdOutlineSearch} from "react-icons/md";
+import IconButton from "./IconButton.tsx";
+import {logoutApi} from "../../common/apis/member.tsx";
+import {logout} from "../../common/slices/loginSlice.tsx";
function Header() {
const loginState = useSelector((state: RootState) => state.loginSlice);
+ const navigate = useNavigate();
+
+ const dashboardRef = useRef(null);
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleDropDown = () => {
+ setIsOpen(cur => !cur);
+ }
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dashboardRef.current && !dashboardRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+
+ const handleLogout = () => {
+ logoutApi(loginState.email)
+ .then(() => {
+ logout();
+ })
+ .catch((error) => {
+ alert(`알 수 없는 에러 : ${error.message}`);
+ })
+ .finally(() => {
+ navigate(0);
+ });
+ }
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
return (
-
-
-
-

+
+
+

+
+ DIGLOG
- {loginState.isLogin
- ?
-
- {loginState.email}, {loginState.username}
-
-
- :
-
- 로그인
+
+
}
+ onClick={() => navigate("/search")}/>
+ {loginState.isLogin
+ ?
+
+
})
+
+
+
{loginState.username}
+
{loginState.email}
+
+
+
+ 게시글 작성
+
+
+ 내 블로그
+
+
+ 설정
+
+
+
+
+
+
+
- }
+ :
+
+
+
}
+
);
}
diff --git a/src/components/common/IconButton.tsx b/src/components/common/IconButton.tsx
new file mode 100644
index 0000000..4c9b0f6
--- /dev/null
+++ b/src/components/common/IconButton.tsx
@@ -0,0 +1,11 @@
+import {ReactNode} from 'react';
+
+function IconButton({icon, onClick}: { icon: ReactNode; onClick: () => void }) {
+ return (
+
+ );
+}
+
+export default IconButton;
\ No newline at end of file
diff --git a/src/components/common/OutlineButton.tsx b/src/components/common/OutlineButton.tsx
new file mode 100644
index 0000000..f2735e0
--- /dev/null
+++ b/src/components/common/OutlineButton.tsx
@@ -0,0 +1,19 @@
+import {Link} from "react-router-dom";
+
+const className = "bg-transparent hover:bg-lime-300 text-lime-700 font-semibold hover:text-white py-2 px-4 border border-lime-500 hover:border-transparent rounded hover:cursor-pointer";
+
+export function OutlineButton({text, onClick, addStyle}: { text: string, onClick?: () => void, addStyle?: string }) {
+ return (
+
+ );
+}
+
+export function OutlineLink({text, to}: { text: string, to: string }) {
+ return (
+
+ {text}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/common/PaginationButton.tsx b/src/components/common/PaginationButton.tsx
new file mode 100644
index 0000000..c8f8082
--- /dev/null
+++ b/src/components/common/PaginationButton.tsx
@@ -0,0 +1,61 @@
+import {PageResponse} from "../../common/types/common.tsx";
+import {MdArrowLeft, MdArrowRight} from "react-icons/md";
+import {useEffect, useState} from "react";
+
+function PaginationButton({pageInfo, setPage}: {
+ pageInfo: PageResponse,
+ setPage: (page: number) => void,
+}) {
+
+ const paginationSize = 5;
+
+ const [pageList, setPageList] = useState
([]);
+ const [startPage, setStartPage] = useState(0);
+
+ const getArray = (start: number, size: number) => {
+ return Array.from({length: size}, (_, index) => index + start);
+ }
+
+ useEffect(() => {
+ if (pageInfo.totalPages < startPage + paginationSize) {
+ setPageList(getArray(startPage, pageInfo.totalPages - startPage));
+ } else {
+ setPageList(getArray(startPage, paginationSize));
+ }
+ }, [startPage]);
+
+ return (
+
+
+ {pageList.map((page) => (
+
+ ))}
+
+
+ );
+}
+
+export default PaginationButton;
\ No newline at end of file
diff --git a/src/components/common/TextButton.tsx b/src/components/common/TextButton.tsx
new file mode 100644
index 0000000..64f7e80
--- /dev/null
+++ b/src/components/common/TextButton.tsx
@@ -0,0 +1,20 @@
+import {Link} from "react-router-dom";
+
+const className = " flex justify-center items-center bg-transparent py-2 px-4 hover:cursor-pointer";
+
+export function TextButton({text, onClick, addStyle}: { text: string, onClick?: () => void, addStyle?: string }) {
+ return (
+
+ );
+}
+
+export function TextLink({text, to, addStyle}: { text: string, to: string, addStyle?: string }) {
+
+ return (
+
+ {text}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/member/LoginButton.tsx b/src/components/member/LoginButton.tsx
new file mode 100644
index 0000000..3167e69
--- /dev/null
+++ b/src/components/member/LoginButton.tsx
@@ -0,0 +1,19 @@
+import {ReactNode} from "react";
+
+function LoginButton({text, onClick, bgColor, icon}: {
+ text: string,
+ onClick: () => void,
+ bgColor: string,
+ icon?: ReactNode
+}) {
+ return (
+
+ );
+}
+
+export default LoginButton;
\ No newline at end of file
diff --git a/src/components/member/LoginTextField.tsx b/src/components/member/LoginTextField.tsx
new file mode 100644
index 0000000..e41c4d2
--- /dev/null
+++ b/src/components/member/LoginTextField.tsx
@@ -0,0 +1,26 @@
+import * as React from "react";
+
+function LoginTextField({label, type, placeholder, value, setValue, onKeyDown}: {
+ label?: string,
+ type: string,
+ placeholder: string,
+ value: string,
+ setValue: (value: string) => void,
+ onKeyDown?: (event: React.KeyboardEvent) => void,
+}) {
+ return (
+
+
+ setValue(event.target.value))}
+ onKeyDown={onKeyDown}/>
+
+ );
+}
+
+export default LoginTextField;
\ No newline at end of file
diff --git a/src/components/post/PostCard.tsx b/src/components/post/PostCard.tsx
new file mode 100644
index 0000000..31f8c8a
--- /dev/null
+++ b/src/components/post/PostCard.tsx
@@ -0,0 +1,47 @@
+import {PostResponse} from "../../common/types/post.tsx";
+import {getImgSrc, removeImgTags} from "../../common/util/html.tsx";
+import DOMPurify from "dompurify";
+import {MdImage} from "react-icons/md";
+import {dateToKorean} from "../../common/util/date.tsx";
+import {Link} from "react-router-dom";
+
+function PostCard(post: PostResponse) {
+
+ const safeContent = DOMPurify.sanitize(post.content);
+ const url = getImgSrc(safeContent);
+
+ return (
+
+
+ {(url)
+ ?

+ :
+
+
}
+
+
+
+
+ {post.username}
+
+
+ {dateToKorean(post.createdAt)}
+
+
+
+
+ {post.title}
+
+
+
+ {removeImgTags(safeContent)}
+
+
+
+ );
+}
+
+export default PostCard;
\ No newline at end of file
diff --git a/src/components/post/TagCard.tsx b/src/components/post/TagCard.tsx
new file mode 100644
index 0000000..1902c5e
--- /dev/null
+++ b/src/components/post/TagCard.tsx
@@ -0,0 +1,14 @@
+import {TagResponse} from "../../common/types/post.tsx";
+
+function TagCard({tag, onClick}: { tag: TagResponse, onClick: (tagName: string) => void }) {
+ return (
+
+ );
+}
+
+export default TagCard;
\ No newline at end of file
diff --git a/src/components/post/TagChip.tsx b/src/components/post/TagChip.tsx
new file mode 100644
index 0000000..ea9a5d4
--- /dev/null
+++ b/src/components/post/TagChip.tsx
@@ -0,0 +1,14 @@
+import {MdOutlineClear} from "react-icons/md";
+
+function TagChip({name, removeTag}: { name: string, removeTag: (selectTag: string) => void }) {
+ return (
+
+ );
+}
+
+export default TagChip;
\ No newline at end of file
diff --git a/src/components/setting/CategoryAddModal.tsx b/src/components/setting/CategoryAddModal.tsx
new file mode 100644
index 0000000..d539b3d
--- /dev/null
+++ b/src/components/setting/CategoryAddModal.tsx
@@ -0,0 +1,94 @@
+import ModalLayout from "../../layout/ModalLayout.tsx";
+import {useState} from "react";
+import {FillButton} from "../common/FillButton.tsx";
+import {CategoryType} from "../../pages/setting/SettingPage.tsx";
+import {TextButton} from "../common/TextButton.tsx";
+import {MdOutlineArrowDropDown} from "react-icons/md";
+
+function CategoryAddModal({categories, setShowCategoryAddModal}: {
+ categories: CategoryType[],
+ setShowCategoryAddModal: (modal: boolean) => void
+}) {
+
+ const [inputCategory, setInputCategory] = useState("");
+ const [categoryOpen, setCategoryOpen] = useState(false);
+ const [selectedCategory, setSelectedCategory] = useState("블로그");
+
+ return (
+
+
+
카테고리 추가
+
+
+
+
+
+ {categories.map((category) => {
+ if (!category.subCategories) {
+ return
+
+
+ } else {
+ return
+
+ {category.subCategories.map((subCategory: CategoryType) =>
+
+ )}
+
;
+ }
+ })}
+
+
setInputCategory(e.target.value)}
+ placeholder="카테고리 이름을 입력해주세요."
+ className="border border-gray-400 p-4 font-bold"/>
+
+ setShowCategoryAddModal(false)}/>
+ {
+ alert("카테고리가 추가되었습니다.");
+ setShowCategoryAddModal(false);
+ }}/>
+
+
+
+ );
+}
+
+export default CategoryAddModal;
\ No newline at end of file
diff --git a/src/components/setting/CategoryCard.tsx b/src/components/setting/CategoryCard.tsx
new file mode 100644
index 0000000..ef83fc8
--- /dev/null
+++ b/src/components/setting/CategoryCard.tsx
@@ -0,0 +1,102 @@
+import {CategoryType} from "../../pages/setting/SettingPage.tsx";
+import {DndContext, DragEndEvent} from "@dnd-kit/core";
+import {SortableContext, useSortable} from "@dnd-kit/sortable";
+import {CSS} from "@dnd-kit/utilities";
+import {MdOutlineMenu} from "react-icons/md";
+import {TextButton} from "../common/TextButton.tsx";
+import {restrictToVerticalAxis} from "@dnd-kit/modifiers";
+import {useState} from "react";
+import {FillButton} from "../common/FillButton.tsx";
+
+function CategoryCard({setSelectedCategory, category, setShowModal, handleDrag, isHover, handleHover, isSub}: {
+ setSelectedCategory: (category: CategoryType) => void,
+ category: CategoryType,
+ setShowModal: (modal: boolean) => void,
+ handleDrag?: (event: DragEndEvent) => void,
+ isHover: boolean,
+ handleHover: (hover: boolean) => void,
+ isSub?: boolean
+}) {
+
+ const [isEdit, setIsEdit] = useState(false);
+ const [categoryNameInput, setCategoryNameInput] = useState(category.name);
+
+ const {attributes, listeners, setNodeRef, transform, transition} = useSortable({id: category.id});
+
+ const style = {
+ transform: CSS.Translate.toString(transform),
+ transition,
+ };
+
+ const handleCategoryNameChange = () => {
+ category.name = categoryNameInput;
+ setIsEdit(false);
+ }
+
+ return (
+
+
{
+ if (isSub) {
+ handleHover(true);
+ }
+ }}
+ onMouseLeave={() => {
+ if (isSub) {
+ handleHover(false);
+ }
+ }}>
+
+ {(isEdit)
+ ?
{
+ setCategoryNameInput(e.target.value)
+ }}/>
+ :
{category.name}
}
+
+ {(isSub && !isEdit) &&
+ {
+ setSelectedCategory(category);
+ setShowModal(true);
+ }}/>}
+ {(isEdit)
+ ?
+ : setIsEdit(true)}/>}
+
+
+ {category.subCategories && (
+
+
+ {category.subCategories.map((subCategory) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+export default CategoryCard;
\ No newline at end of file
diff --git a/src/components/setting/CategoryMoveModal.tsx b/src/components/setting/CategoryMoveModal.tsx
new file mode 100644
index 0000000..a7b62d6
--- /dev/null
+++ b/src/components/setting/CategoryMoveModal.tsx
@@ -0,0 +1,53 @@
+import {CategoryType} from "../../pages/setting/SettingPage.tsx";
+import {FillButton} from "../common/FillButton.tsx";
+import {useState} from "react";
+import {TextButton} from "../common/TextButton.tsx";
+import {useSelector} from "react-redux";
+import {RootState} from "../../store.tsx";
+import ModalLayout from "../../layout/ModalLayout.tsx";
+
+function CategoryMoveModal({selectedCategory, categories, handleCategoryMove, setShowModal}: {
+ selectedCategory: CategoryType | null,
+ categories: CategoryType[],
+ handleCategoryMove: (categoryId: string) => void
+ setShowModal: (showModal: boolean) => void,
+}) {
+
+ const loginState = useSelector((state: RootState) => state.loginSlice);
+
+ const [selectedCategoryId, setSelectedCategoryId] = useState("");
+
+ const handleCategoryChange = () => {
+ handleCategoryMove(selectedCategoryId);
+
+ setShowModal(false);
+ }
+
+ return (
+
+
+
+ "{selectedCategory?.name}" 카테고리를 이동할 곳을 골라주세요.
+
+
+ {categories.map(category => (
+
+ ))}
+
+ setShowModal(false)}/>
+
+
+
+
+ );
+}
+
+export default CategoryMoveModal;
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index a461c50..143a76d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1 +1,69 @@
-@import "tailwindcss";
\ No newline at end of file
+@import "tailwindcss";
+
+@theme {
+ --font-*: initial;
+ --default-font-family: 'SCDream';
+ --font-jalnan: 'Jalnan2';
+}
+
+@font-face {
+ font-family: 'Jalnan2';
+ src: url('./common/assets/fonts/Jalnan2.otf');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'SCDream';
+ src: url('./common/assets/fonts/s-core/SCDream1.otf');
+ font-weight: 100;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'SCDream';
+ src: url('./common/assets/fonts/s-core/SCDream2.otf');
+ font-weight: 200;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'SCDream';
+ src: url('./common/assets/fonts/s-core/SCDream3.otf');
+ font-weight: 300;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'SCDream';
+ src: url('./common/assets/fonts/s-core/SCDream4.otf');
+ font-weight: 400;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'SCDream';
+ src: url('./common/assets/fonts/s-core/SCDream5.otf');
+ font-weight: 500;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'SCDream';
+ src: url('./common/assets/fonts/s-core/SCDream6.otf');
+ font-weight: 600;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'SCDream';
+ src: url('./common/assets/fonts/s-core/SCDream7.otf');
+ font-weight: 700;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'SCDream';
+ src: url('./common/assets/fonts/s-core/SCDream8.otf');
+ font-weight: 800;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'SCDream';
+ src: url('./common/assets/fonts/s-core/SCDream9.otf');
+ font-weight: 900;
+ font-style: normal;
+}
\ No newline at end of file
diff --git a/src/layout/BasicLayout.tsx b/src/layout/BasicLayout.tsx
index 3f7072c..1803c7b 100644
--- a/src/layout/BasicLayout.tsx
+++ b/src/layout/BasicLayout.tsx
@@ -1,14 +1,16 @@
import {ReactNode} from "react";
import Header from "../components/common/Header.tsx";
+import Footer from "../components/common/Footer.tsx";
-function BasicLayout({children}: { children: ReactNode }) {
+function BasicLayout({children, center}: { children: ReactNode, center?: boolean }) {
return (
-
-
-
+
+
+
{children}
+
);
}
diff --git a/src/layout/ModalLayout.tsx b/src/layout/ModalLayout.tsx
new file mode 100644
index 0000000..131b304
--- /dev/null
+++ b/src/layout/ModalLayout.tsx
@@ -0,0 +1,14 @@
+import {ReactNode} from "react";
+
+function ModalLayout({children}: { children: ReactNode }) {
+ return (
+
+ );
+}
+
+export default ModalLayout;
\ No newline at end of file
diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx
index 7fb8a3e..25406f4 100644
--- a/src/pages/MainPage.tsx
+++ b/src/pages/MainPage.tsx
@@ -1,9 +1,11 @@
import BasicLayout from "../layout/BasicLayout.tsx";
import {useEffect} from "react";
-import {getProfile} from "../apis/member.tsx";
-import {login} from "../slices/loginSlice.tsx";
+import {getProfile} from "../common/apis/member.tsx";
+import {login} from "../common/slices/loginSlice.tsx";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "../store.tsx";
+import PostCard from "../components/post/PostCard.tsx";
+import {faker} from "@faker-js/faker/locale/ko";
function MainPage() {
@@ -25,10 +27,39 @@ function MainPage() {
});
}, []);
+
return (
-
- MainPage
+
+
+ 인기 있는 게시글
+
+
+ {[Array.from({length: 6}).map(() => (
+
`}
+ username={faker.animal.cat()}
+ tags={[{
+ id: faker.number.int().toString(),
+ name: faker.word.sample()
+ }, {id: faker.number.int().toString(), name: faker.word.sample()}]}
+ createdAt={new Date()}/>
+ ))]}
+
+
);
diff --git a/src/pages/blog/BlogPage.tsx b/src/pages/blog/BlogPage.tsx
new file mode 100644
index 0000000..8e33a3a
--- /dev/null
+++ b/src/pages/blog/BlogPage.tsx
@@ -0,0 +1,201 @@
+import BasicLayout from "../../layout/BasicLayout.tsx";
+import {useParams, useSearchParams} from "react-router-dom";
+import {faker} from "@faker-js/faker/locale/ko";
+import PostCard from "../../components/post/PostCard.tsx";
+import TagCard from "../../components/post/TagCard.tsx";
+import {useEffect, useRef, useState} from "react";
+import {MdMenu, MdOutlineExitToApp} from "react-icons/md";
+import PaginationButton from "../../components/common/PaginationButton.tsx";
+import TagChip from "../../components/post/TagChip.tsx";
+
+function BlogPage() {
+
+ const {username} = useParams();
+
+ const [searchParams, setSearchParams] = useSearchParams({"category": ""});
+
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedCategory, setSelectedCategory] = useState(searchParams.get("category") || "");
+ const [selectedTagList, setSelectedTagList] = useState
([]);
+
+ const mainRef = useRef(null);
+ const sideBarRef = useRef(null);
+
+ const handleMenuOpen = () => {
+ setIsOpen(cur => !cur);
+ }
+ const handleClickOutside = (event: MouseEvent) => {
+ if (sideBarRef.current && !sideBarRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+
+ const addTag = (selectTag: string) => {
+ setSelectedTagList([...selectedTagList, selectTag]);
+ }
+
+ const removeTag = (selectTag: string) => {
+ setSelectedTagList(prevState => prevState.filter(tag => tag !== selectTag));
+ }
+
+ const removeCategory = (selectCategory: string) => {
+ console.log(selectCategory);
+ setSelectedCategory("");
+ }
+
+ useEffect(() => {
+ if (sideBarRef.current && mainRef.current) {
+ sideBarRef.current.style.height = `${mainRef.current.offsetHeight + 220}px`;
+ }
+ }, []);
+
+
+ useEffect(() => {
+ document.addEventListener('mousedown', handleClickOutside);
+ document.title = username || "DIGLOG";
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ document.title = "DIGLOG";
+ };
+ }, []);
+
+ useEffect(() => {
+ setSearchParams({
+ "category": selectedCategory
+ })
+ }, [selectedCategory]);
+
+ useEffect(() => {
+ if (isOpen) {
+ document.body.classList.remove('overflow-x-hidden');
+ } else {
+ document.body.classList.add('overflow-x-hidden');
+ }
+
+ return () => {
+ document.body.classList.remove('overflow-x-hidden');
+ };
+ }, [isOpen]);
+
+ return (
+
+
+
+
{username}의 블로그
+
+
+
+
+ 카테고리 {selectedCategory !== "" && }
+
+
+ 태그 {selectedTagList.map((tag) => )}
+
+
+
+
+ {[Array.from({length: 3}).map(() => (
+
`}
+ username={faker.animal.cat()}
+ tags={[{
+ id: faker.number.int().toString(),
+ name: faker.word.sample()
+ }, {id: faker.number.int().toString(), name: faker.word.sample()}]}
+ createdAt={new Date()}/>
+ ))]}
+
{
+ }}/>
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function SideBar({username, addTag, setSelectedCategory, bgColor}: {
+ username: string | undefined,
+ addTag: (tagName: string) => void,
+ setSelectedCategory: (category: string) => void,
+ bgColor?: string
+}) {
+
+ return (
+
+
+
})
+
+ {username}
+
+
+
+
카테고리
+
+ {[Array.from({length: 10}).map((_, i) => (
+
+
+
+ {Array.from({length: 3}).map((_, i) => (
+
+ ))}
+
+
+ ))]}
+
+
+
+
태그
+
+ {[Array.from({length: 24}).map(() => (
+
+ ))]}
+
+
+
+ );
+}
+
+export default BlogPage;
\ No newline at end of file
diff --git a/src/pages/member/CodePage.tsx b/src/pages/member/CodePage.tsx
new file mode 100644
index 0000000..5eaed50
--- /dev/null
+++ b/src/pages/member/CodePage.tsx
@@ -0,0 +1,109 @@
+import React, {useEffect, useRef, useState} from "react";
+import BasicLayout from "../../layout/BasicLayout.tsx";
+import {useLocation, useNavigate} from "react-router-dom";
+import {formatTimer} from "../../common/util/date.tsx";
+import LoadingLayout from "../../layout/LoadingLayout.tsx";
+
+function CodePage() {
+
+ const {state} = useLocation();
+ const {email} = state;
+ const navigate = useNavigate();
+
+ const [loading, setLoading] = useState(false);
+
+ const inputRefs = useRef([]);
+ const [code, setCode] = useState(Array(6).fill(""));
+
+ const [timer, setTimer] = useState(600);
+
+ const handleChange = (index: number, value: string) => {
+ if (/[^0-9]/g.test(value)) {
+ return;
+ }
+
+ const newCode = [...code];
+
+ newCode[index] = value.replace(/[^0-9]/g, "");
+ setCode(newCode);
+
+ if (value && index < inputRefs.current.length - 1) {
+ inputRefs.current[index + 1].focus();
+ }
+ };
+
+ const handleBackspace = (event: React.KeyboardEvent, index: number) => {
+ if (event.key === "Backspace" && !event.currentTarget.value && index > 0) {
+ inputRefs.current[index - 1].focus();
+ }
+ };
+
+ useEffect(() => {
+ if (inputRefs.current) {
+ inputRefs.current[0].focus();
+ }
+ }, []);
+
+ useEffect(() => {
+ let interval = null;
+
+ if (timer > 0) {
+ interval = setInterval(() => {
+ setTimer((prev) => prev - 1);
+ }, 1000);
+ } else if (timer === 0) {
+ clearInterval(interval!);
+ alert("유효 시간이 만료되었습니다. 회원가입을 다시 시도해주세요.");
+ }
+
+ return () => clearInterval(interval!);
+ }, [timer]);
+
+ useEffect(() => {
+ if (code[5] !== "") {
+ setLoading(true);
+
+ navigate("/signup", {state: {email: email, code: code.join("")}});
+ }
+ }, [code]);
+
+ return (
+
+
+
+
{email} 로 보내진
+
인증코드 6자리를 입력해주세요.
+
+
+ {code.map((digit, index) => (
+
+ {
+ if (el) {
+ inputRefs.current[index] = el;
+ }
+ }}
+ type=" text"
+ maxLength={1}
+ className="w-12 h-16 rounded-lg font-bold text-lg appearance-none border border-gray-200 text-center focus:outline-1"
+ value={digit}
+ onChange={(e) => handleChange(index, e.target.value)}
+ onKeyDown={(e) => {
+ handleBackspace(e, index)
+ }}
+ />
+
+ ))}
+
+
+ 유효시간 : {formatTimer(timer)}
+
+
+
+
+ )
+ ;
+}
+
+export default CodePage;
\ No newline at end of file
diff --git a/src/pages/member/EmailPage.tsx b/src/pages/member/EmailPage.tsx
new file mode 100644
index 0000000..e834a00
--- /dev/null
+++ b/src/pages/member/EmailPage.tsx
@@ -0,0 +1,49 @@
+import BasicLayout from "../../layout/BasicLayout.tsx";
+import LoginTextField from "../../components/member/LoginTextField.tsx";
+import {useState} from "react";
+import LoginButton from "../../components/member/LoginButton.tsx";
+import {useNavigate} from "react-router-dom";
+import * as React from "react";
+import {checkEmail} from "../../common/util/regex.tsx";
+
+function EmailPage() {
+
+ const [signupInfo, setSignupInfo] = useState({email: "", code: "", password: ""});
+
+ const navigate = useNavigate();
+
+ const handleEmailEnter = (event: React.KeyboardEvent) => {
+ if (event.key !== "Enter") {
+ return;
+ }
+
+ handleVerifyEmail();
+ }
+
+ const handleVerifyEmail = () => {
+ if (!checkEmail(signupInfo.email)) {
+ alert("이메일 형식을 확인해주세요.");
+ return;
+ }
+
+ navigate("/signup/code", {state: {email: signupInfo.email}});
+ }
+
+ return (
+
+
+ setSignupInfo({...signupInfo, email: value})}
+ onKeyDown={handleEmailEnter}/>
+
+
+
+ );
+}
+
+export default EmailPage;
\ No newline at end of file
diff --git a/src/pages/member/LoginPage.tsx b/src/pages/member/LoginPage.tsx
index abd1aa0..0d00df2 100644
--- a/src/pages/member/LoginPage.tsx
+++ b/src/pages/member/LoginPage.tsx
@@ -2,8 +2,13 @@ import BasicLayout from "../../layout/BasicLayout.tsx";
import {useNavigate} from "react-router-dom";
import {useState} from "react";
import {useDispatch} from "react-redux";
-import {handleKakaoLogin, loginApi} from "../../apis/member.tsx";
-import {login} from "../../slices/loginSlice.tsx";
+import {handleKakaoLogin, loginApi} from "../../common/apis/member.tsx";
+import {login} from "../../common/slices/loginSlice.tsx";
+import LoginButton from "../../components/member/LoginButton.tsx";
+import LoginTextField from "../../components/member/LoginTextField.tsx";
+import {checkEmail} from "../../common/util/regex.tsx";
+import * as React from "react";
+import {TextLink} from "../../components/common/TextButton.tsx";
function LoginPage() {
@@ -28,26 +33,50 @@ function LoginPage() {
})
}
+ const handlePasswordEnter = (event: React.KeyboardEvent) => {
+ if (event.key !== "Enter") {
+ return;
+ }
+ if (!checkEmail(loginInfo.email)) {
+ alert("이메일 형식을 확인해주세요.");
+ return;
+ }
+
+ handleLogin();
+ }
+
return (
-
-
-
setLoginInfo({...loginInfo, email: e.target.value})}/>
-
setLoginInfo({...loginInfo, password: e.target.value})}/>
-
-
+
+
+
+

+
+ DIGLOG
+
+
+
+ setLoginInfo({...loginInfo, email: value})}/>
+ setLoginInfo({...loginInfo, password: value})}
+ onKeyDown={handlePasswordEnter}/>
+
+
+
+ }/>
+
+
);
diff --git a/src/pages/member/PlatformPage.tsx b/src/pages/member/PlatformPage.tsx
new file mode 100644
index 0000000..77a588b
--- /dev/null
+++ b/src/pages/member/PlatformPage.tsx
@@ -0,0 +1,19 @@
+import BasicLayout from "../../layout/BasicLayout.tsx";
+import LoginButton from "../../components/member/LoginButton.tsx";
+import {handleKakaoLogin} from "../../common/apis/member.tsx";
+import {TextLink} from "../../components/common/TextButton.tsx";
+
+function PlatformPage() {
+
+ return (
+
+
+ }/>
+
+
+
+ );
+}
+
+export default PlatformPage;
\ No newline at end of file
diff --git a/src/pages/member/SignupPage.tsx b/src/pages/member/SignupPage.tsx
new file mode 100644
index 0000000..b5aa726
--- /dev/null
+++ b/src/pages/member/SignupPage.tsx
@@ -0,0 +1,69 @@
+import BasicLayout from "../../layout/BasicLayout.tsx";
+import {useLocation, useNavigate} from "react-router-dom";
+import {useState} from "react";
+import LoginTextField from "../../components/member/LoginTextField.tsx";
+import {checkPassword} from "../../common/util/regex.tsx";
+import LoadingLayout from "../../layout/LoadingLayout.tsx";
+import * as React from "react";
+import LoginButton from "../../components/member/LoginButton.tsx";
+
+function SignupPage() {
+
+ const navigate = useNavigate();
+ const {state} = useLocation();
+ const {email, code} = state;
+
+ const [loading, setLoading] = useState(false);
+ const [passwordInfo, setPasswordInfo] = useState({password: "", confirmPassword: ""});
+
+ const handlePasswordEnter = (event: React.KeyboardEvent
) => {
+ if (event.key !== "Enter") {
+ return;
+ }
+
+ handleSignup();
+ }
+
+ const handleSignup = () => {
+ setLoading(true);
+ console.log(email, code);
+
+ if (!checkPassword(passwordInfo.password) || passwordInfo.password !== passwordInfo.confirmPassword) {
+ setLoading(false);
+ return;
+ }
+
+ alert("회원가입 되었습니다.");
+
+ navigate("/login");
+ }
+
+ return (
+
+
+
비밀번호 설정을 마치면 회원가입이 완료됩니다.
+
+ setPasswordInfo({...passwordInfo, password: value})}/>
+ setPasswordInfo({...passwordInfo, confirmPassword: value})}
+ onKeyDown={handlePasswordEnter}/>
+
+
+
+
+
+ )
+ ;
+}
+
+export default SignupPage;
\ No newline at end of file
diff --git a/src/pages/post/PostPage.tsx b/src/pages/post/PostPage.tsx
new file mode 100644
index 0000000..4dfcc50
--- /dev/null
+++ b/src/pages/post/PostPage.tsx
@@ -0,0 +1,67 @@
+import {Link, useNavigate, useParams} from "react-router-dom";
+import BasicLayout from "../../layout/BasicLayout.tsx";
+import {PostResponse} from "../../common/types/post.tsx";
+import {faker} from "@faker-js/faker/locale/ko";
+import DOMPurify from "dompurify";
+import {fullDateToKorean} from "../../common/util/date.tsx";
+import TagCard from "../../components/post/TagCard.tsx";
+
+function PostPage() {
+
+ const {id} = useParams();
+ console.log(id);
+ const navigate = useNavigate();
+
+ const post: PostResponse = {
+ id: faker.number.int().toString(),
+ title: faker.lorem.sentence(),
+ content: `
${faker.lorem.paragraphs()}
`,
+ username: faker.animal.cat(),
+ tags: [{id: "1", name: faker.lorem.word()}, {id: "2", name: faker.lorem.word()}],
+ createdAt: new Date(),
+ };
+
+ const safeContent = DOMPurify.sanitize(post.content);
+
+ return (
+
+
+
+
+
Home
+
{` > `}
+
카테고리
+
{` > `}
+
{post.title}
+
+
+
+ {post.username}
+
+
+ {fullDateToKorean(post.createdAt)}
+
+
+
+ {post.title}
+
+
+ {post.tags.map(tag =>
+ {
+ navigate(`/search?word=${tag.name}&option=태그`)
+ }}/>)}
+
+
+
+
+
+
+ );
+}
+
+export default PostPage;
\ No newline at end of file
diff --git a/src/pages/post/SearchPage.tsx b/src/pages/post/SearchPage.tsx
new file mode 100644
index 0000000..129bab1
--- /dev/null
+++ b/src/pages/post/SearchPage.tsx
@@ -0,0 +1,266 @@
+import BasicLayout from "../../layout/BasicLayout.tsx";
+import {MdArrowDropDown, MdOutlineClear, MdOutlineSearch} from "react-icons/md";
+import {Ref, useEffect, useRef, useState} from "react";
+import * as React from "react";
+import PostCard from "../../components/post/PostCard.tsx";
+import {faker} from "@faker-js/faker/locale/ko";
+import {Link, useSearchParams} from "react-router-dom";
+import {LoadMoreButton} from "../../components/common/FillButton.tsx";
+
+function SearchPage() {
+
+ const optionRef = useRef(null);
+ const sortRef = useRef(null);
+
+ const [openOption, setOpenOption] = useState(false);
+ const [openSort, setOpenSort] = useState(false);
+
+ const [searchParams, setSearchParams] = useSearchParams({"word": "", "option": "전체", "sort": "최신순", "tab": "게시글"});
+ const [searchWord, setSearchWord] = useState(searchParams.get("word") || "");
+ const [option, setOption] = useState(searchParams.get("option") || "전체");
+ const [sort, setSort] = useState(searchParams.get("sort") || "최신순");
+ const [selectedTab, setSelectedTab] = useState(searchParams.get("tab") || "게시글");
+
+ const handleOpenOption = () => {
+ setOpenOption(cur => !cur);
+ }
+ const handleOpenSort = () => {
+ setOpenSort(cur => !cur);
+ }
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (optionRef.current && !optionRef.current.contains(event.target as Node)) {
+ setOpenOption(false);
+ }
+ if (sortRef.current && !sortRef.current.contains(event.target as Node)) {
+ setOpenSort(false);
+ }
+ };
+
+ const handleSearchEnter = (event: React.KeyboardEvent) => {
+ if (event.key !== "Enter") {
+ return;
+ }
+ handleSearchWord();
+ }
+
+ const handleSearchWord = () => {
+ if (searchWord === "") {
+ return;
+ }
+
+ setSearchParams({
+ "word": searchWord,
+ "option": option,
+ "sort": sort,
+ "tab": selectedTab,
+ });
+ }
+
+ useEffect(() => {
+ setSearchParams({
+ "word": searchWord,
+ "option": option,
+ "sort": sort,
+ "tab": selectedTab,
+ });
+ }, [option, sort, selectedTab]);
+
+ useEffect(() => {
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+ setSearchWord(e.target.value)}
+ placeholder={"검색어를 입력해주세요."}
+ className="w-full block mt-0.5 p-3 mr-4 font-jalnan text-xl text-gray-900 border-b-2 border-white focus:outline-none focus:border-black"
+ onKeyDown={handleSearchEnter}/>
+
+
+
+
+
+
+
+
+ );
+}
+
+function SearchTab({selectedTab, setSelectedTab}: {
+ selectedTab: string,
+ setSelectedTab: (tab: string) => void,
+}) {
+
+ const tabs = ["게시글", "블로그"];
+
+ return (
+
+
+ {tabs.map((tab) => (
+ -
+
+
+ ))}
+
+
+ );
+}
+
+function SearchResults({selectedTab}: { selectedTab: string }) {
+
+ if (selectedTab === "게시글") {
+ return (
+
+ {(Array.from({length: 5}).map(() => (
+
`}
+ username={faker.animal.cat()}
+ tags={[{
+ id: faker.number.int().toString(),
+ name: faker.word.sample()
+ }, {id: faker.number.int().toString(), name: faker.word.sample()}]}
+ createdAt={new Date()}/>
+ )))}
+
{
+ }} addStyle={"w-full"}/>
+
+ );
+ } else if (selectedTab === "블로그") {
+ return (
+
+ {(Array.from({length: 5}).map(() => (
+
+
})
+
+
{faker.animal.dog()}
+
{faker.animal.dog()}
+
{faker.animal.dog()}
+
+
+ )))}
+
{
+ }}/>
+
+ );
+ }
+
+ return <>>;
+}
+
+function SearchMenu({type, open, handleOpen, value, setValue, customRef}: {
+ type: string,
+ open: boolean,
+ handleOpen: () => void,
+ value: string,
+ setValue: (newOption: string) => void,
+ customRef: Ref,
+}) {
+
+ let title = "";
+ let searchMenuList: string[] = [];
+ if (type === "option") {
+ title = "검색 조건";
+ searchMenuList = ["전체", "제목", "내용", "태그", "작성자"];
+ } else if (type === "sort") {
+ title = "정렬 조건";
+ searchMenuList = ["최신순", "오래된순", "조회순"];
+ }
+
+ return (
+
+
+
{title}
+
+
+
+
+ {searchMenuList.map((menu) => (
+
+ ))}
+
+
+
+
+
+ );
+}
+
+function SortMenu({value, current, setValue, handleOpen}: {
+ value: string,
+ current: string,
+ setValue: (newSort: string) => void,
+ handleOpen: () => void
+}) {
+ return (
+
+
+
+ );
+}
+
+export default SearchPage;
\ No newline at end of file
diff --git a/src/pages/post/WritePage.tsx b/src/pages/post/WritePage.tsx
new file mode 100644
index 0000000..919a164
--- /dev/null
+++ b/src/pages/post/WritePage.tsx
@@ -0,0 +1,234 @@
+import BasicLayout from "../../layout/BasicLayout.tsx";
+import {useEffect, useRef, useState} from "react";
+import {useBlocker, useNavigate} from "react-router-dom";
+import {Editor} from "@tinymce/tinymce-react";
+import {FillButton} from "../../components/common/FillButton.tsx";
+import {useSelector} from "react-redux";
+import {RootState} from "../../store.tsx";
+import {faker} from "@faker-js/faker/locale/ko";
+import {MdOutlineArrowDropDown, MdOutlineClear} from "react-icons/md";
+
+interface WritePostType {
+ inputTag: string;
+ tags: string[];
+ title: string;
+ content: string;
+}
+
+interface CategoryType {
+ name: string;
+ subCategories?: CategoryType[];
+}
+
+function WritePage() {
+
+ const loginState = useSelector((state: RootState) => state.loginSlice);
+ const navigate = useNavigate();
+ const categoryRef = useRef(null);
+
+ const [post, setPost] = useState({
+ inputTag: "",
+ tags: [],
+ title: "",
+ content: "",
+ });
+ const [showTag, setShowTag] = useState(false);
+
+ const [categoryOpen, setCategoryOpen] = useState(false);
+ const [selectedCategory, setSelectedCategory] = useState("카테고리 선택");
+ const [uploadCount, setUploadCount] = useState(0);
+ const [exitPage, setExitPage] = useState(false);
+ const handleCategoryOpen = () => {
+ setCategoryOpen(prev => !prev);
+ }
+
+ const removeTag = (tag: string | null) => {
+ setPost({...post, tags: post.tags.filter(prevTag => prevTag !== tag)});
+ }
+ const handleTag = (tag: string) => {
+ if (!tag.endsWith(",")) {
+ setPost({...post, inputTag: tag});
+ return;
+ }
+ if (post.tags.includes(tag.substring(0, tag.length - 1))) {
+ setPost({...post, inputTag: ""});
+ return;
+ }
+ setPost({...post, tags: [...post.tags, post.inputTag], inputTag: ""});
+ }
+
+ const handleSubmit = () => {
+ if (uploadCount > 0) {
+ alert("업로드 중인 이미지가 있습니다. 잠시만 기다려주세요.");
+ return;
+ }
+
+ alert("작성되었습니다.");
+ setExitPage(true);
+ navigate(`/blog/${loginState.username}username`);
+ }
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (categoryRef.current && !categoryRef.current.contains(event.target as Node)) {
+ setCategoryOpen(false);
+ }
+ };
+
+ // 뒤로가기 방지
+ useBlocker(() => {
+ return (!!post.title || !!post.content) &&
+ exitPage &&
+ !confirm("페이지를 이동하시겠습니까?\n\n작성중인 내용이 저장되지 않습니다.");
+ }
+ );
+
+ // 새로고침 방지
+ useEffect(() => {
+ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
+ event.preventDefault();
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ window.addEventListener('beforeunload', handleBeforeUnload);
+
+ return () => {
+ window.removeEventListener('beforeunload', handleBeforeUnload);
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const categoryData: CategoryType[] = [
+ {
+ name: faker.lorem.words(),
+ subCategories: [
+ {name: faker.lorem.words()},
+ {name: faker.lorem.words()}
+ ]
+ },
+ {
+ name: faker.lorem.words(),
+ },
+ {
+ name: faker.lorem.words(),
+ subCategories: [
+ {name: faker.lorem.words()},
+ {name: faker.lorem.words()},
+ {name: faker.lorem.words()}
+ ]
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+ {categoryData.map((category) => {
+ if (!category.subCategories) {
+ return
+
+
+ } else {
+ return
+
+ {category.subCategories.map((subCategory: CategoryType) =>
+
+ )}
+
;
+ }
+ })}
+
+
+
+
+ setPost({...post, title: e.target.value})}
+ placeholder={"제목을 입력해주세요."}
+ className="w-full py-2 font-jalnan text-2xl text-gray-900 border-b-2 border-white focus:outline-none focus:border-black"/>
+
+
+
+ {post.tags.map((tag, i) =>
+ )}
+ handleTag(event.target.value)}
+ onFocus={() => setShowTag(true)}
+ onBlur={() => setShowTag(false)}/>
+
+ {showTag &&
+ 쉼표를 입력하여 태그를 등록할 수 있습니다.
+
}
+
+
{
+ setUploadCount((prev) => prev + 1);
+ // const res = await uploadImage(blobInfo);
+ // return res.data.url;
+ console.log("Image Upload...... ", blobInfo);
+ setUploadCount((prev) => prev - 1);
+ return faker.image.url();
+ }
+ }}
+ value={post.content}
+ onEditorChange={(content) => setPost({...post, content: content})}
+ />
+
+
+
+
+
+ );
+}
+
+export default WritePage;
\ No newline at end of file
diff --git a/src/pages/setting/CategorySettingPage.tsx b/src/pages/setting/CategorySettingPage.tsx
new file mode 100644
index 0000000..53bd333
--- /dev/null
+++ b/src/pages/setting/CategorySettingPage.tsx
@@ -0,0 +1,58 @@
+import {DndContext, DragEndEvent} from "@dnd-kit/core";
+import {restrictToVerticalAxis} from "@dnd-kit/modifiers";
+import {SortableContext} from "@dnd-kit/sortable";
+import {FillButton} from "../../components/common/FillButton.tsx";
+import {CategoryType} from "./SettingPage.tsx";
+import CategoryCard from "../../components/setting/CategoryCard.tsx";
+import {OutlineButton} from "../../components/common/OutlineButton.tsx";
+
+function CategorySettingPage({
+ setSelectedCategory,
+ categories,
+ setShowModal,
+ setShowCategoryAddModal,
+ handleDragEnd,
+ isHover,
+ handleHover,
+ submitCategoryChange
+ }: {
+ setSelectedCategory: (category: CategoryType) => void,
+ categories: CategoryType[],
+ setShowModal: (modal: boolean) => void,
+ setShowCategoryAddModal: (modal: boolean) => void,
+ handleDragEnd: (event: DragEndEvent) => void,
+ isHover: boolean,
+ handleHover: (hover: boolean) => void,
+ submitCategoryChange: () => void,
+}) {
+
+ return (
+
+
카테고리 관리
+
+
+
+ {categories.map((category) => (
+
+
+
+ ))}
+
+
+
+
+ setShowCategoryAddModal(true)} addStyle={"font-normal"}/>
+
+
+
+ );
+}
+
+export default CategorySettingPage;
\ No newline at end of file
diff --git a/src/pages/setting/PostSettingPage.tsx b/src/pages/setting/PostSettingPage.tsx
new file mode 100644
index 0000000..f035013
--- /dev/null
+++ b/src/pages/setting/PostSettingPage.tsx
@@ -0,0 +1,9 @@
+function PostSettingPage() {
+ return (
+
+ );
+}
+
+export default PostSettingPage;
\ No newline at end of file
diff --git a/src/pages/setting/ProfileSettingPage.tsx b/src/pages/setting/ProfileSettingPage.tsx
new file mode 100644
index 0000000..3c382d8
--- /dev/null
+++ b/src/pages/setting/ProfileSettingPage.tsx
@@ -0,0 +1,122 @@
+import {useDispatch, useSelector} from "react-redux";
+import {RootState} from "../../store.tsx";
+import {faker} from "@faker-js/faker/locale/ko";
+import {MdOutlineEdit} from "react-icons/md";
+import {ChangeEvent, useRef, useState} from "react";
+import {FillButton} from "../../components/common/FillButton.tsx";
+import {setUsername} from "../../common/slices/loginSlice.tsx";
+import {TextButton} from "../../components/common/TextButton.tsx";
+
+function ProfileSettingPage() {
+
+ const loginState = useSelector((state: RootState) => state.loginSlice);
+ const dispatch = useDispatch();
+ const fileInputRef = useRef(null);
+
+ const [input, setInput] = useState("");
+ const [image, setImage] = useState(null);
+ const [isUsernameEdit, setIsUsernameEdit] = useState(false);
+ const [isImageEdit, setIsImageEdit] = useState(false);
+
+ const handleUsernameEdit = () => {
+ setIsUsernameEdit(true);
+ }
+
+ const handleUsernameEditCancel = () => {
+ setIsUsernameEdit(false);
+ }
+
+ const handleUsernameSubmit = () => {
+ alert("변경되었습니다.");
+ dispatch(setUsername({
+ username: input,
+ }));
+ setIsUsernameEdit(false);
+ }
+
+ const handleImageEdit = () => {
+ setIsImageEdit(true);
+ fileInputRef.current!.click();
+ }
+
+ const handleImageChange = (event: ChangeEvent) => {
+ if (event.target.files && event.target.files[0]) {
+ setImage(URL.createObjectURL(event.target.files[0]));
+ }
+ }
+
+ const handleImageEditCancel = () => {
+ setIsImageEdit(false);
+ }
+
+ const handleImageSubmit = () => {
+ alert("변경되었습니다.");
+ setIsImageEdit(false);
+ }
+
+ return (
+
+
프로필 관리
+
+
+
+
+ {isImageEdit && image
+ ?

+ :
})
}
+
+
+
+
+
+
+ {isImageEdit &&
+
+
+
}
+
+
+ {isUsernameEdit
+ ?
+ setInput(e.target.value)}
+ placeholder="Username"/>
+
+
+
+ :
+
+
+ {loginState.username}
+
+
+
}
+
+
+
+ );
+}
+
+export default ProfileSettingPage;
\ No newline at end of file
diff --git a/src/pages/setting/SettingPage.tsx b/src/pages/setting/SettingPage.tsx
new file mode 100644
index 0000000..87f39e9
--- /dev/null
+++ b/src/pages/setting/SettingPage.tsx
@@ -0,0 +1,209 @@
+import BasicLayout from "../../layout/BasicLayout.tsx";
+import {useState} from "react";
+import {faker} from "@faker-js/faker/locale/ko";
+import {DragEndEvent} from "@dnd-kit/core";
+import {arrayMove} from "@dnd-kit/sortable";
+import {useNavigate} from "react-router-dom";
+import CategorySettingPage from "./CategorySettingPage.tsx";
+import PostSettingPage from "./PostSettingPage.tsx";
+import ProfileSettingPage from "./ProfileSettingPage.tsx";
+import CategoryMoveModal from "../../components/setting/CategoryMoveModal.tsx";
+import CategoryAddModal from "../../components/setting/CategoryAddModal.tsx";
+
+export interface CategoryType {
+ id: string;
+ name: string;
+ subCategories?: CategoryType[];
+}
+
+function SettingPage() {
+
+ const navigate = useNavigate();
+
+ const [showCategoryModal, setShowCategoryModal] = useState(false);
+ const [showCategoryAddModal, setShowCategoryAddModal] = useState(false);
+
+ const tabList = ["프로필", "카테고리", "게시글"];
+ const categoryData: CategoryType[] = [
+ {
+ id: "1",
+ name: faker.lorem.words(),
+ subCategories: [
+ {id: "4", name: faker.lorem.words()},
+ {id: "5", name: faker.lorem.words()}
+ ]
+ },
+ {
+ id: "2",
+ name: faker.lorem.words(),
+ },
+ {
+ id: "3",
+ name: faker.lorem.words(),
+ subCategories: [
+ {id: "6", name: faker.lorem.words()},
+ {id: "7", name: faker.lorem.words()},
+ {id: "8", name: faker.lorem.words()}
+ ]
+ },
+ ];
+
+ const [selectedTab, setSelectedTab] = useState("프로필");
+ const [categories, setCategories] = useState(categoryData);
+ const [selectedCategory, setSelectedCategory] = useState(null);
+ const [isHover, setIsHover] = useState(false);
+
+ const handleHover = (hover: boolean) => {
+ setIsHover(hover);
+ }
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ const {active, over} = event;
+
+ if (!over || active.id === over.id) {
+ return;
+ }
+
+ let categoryIndex = -1;
+ let oldIndex = -1;
+ let newIndex = -1;
+
+ oldIndex = categories.findIndex(category => category.id === active.id);
+ newIndex = categories.findIndex(category => category.id === over.id);
+
+ if (oldIndex !== -1 && newIndex !== -1) {
+ setCategories(category => {
+ return arrayMove(category, oldIndex, newIndex);
+ });
+ return;
+ }
+
+ for (const category of categories) {
+ if (category.subCategories) {
+ oldIndex = category.subCategories.findIndex(category => category.id === active.id);
+ newIndex = category.subCategories.findIndex(category => category.id === over.id);
+
+ if (oldIndex !== -1 && newIndex !== -1) {
+ categoryIndex = categories.indexOf(category);
+ break;
+ }
+ }
+ }
+
+ if (categoryIndex !== -1 && oldIndex !== -1 && newIndex !== -1) {
+ setCategories(currentCategories => {
+ const updatedSubCategories = [...currentCategories[categoryIndex].subCategories!];
+ const [movedCategory] = updatedSubCategories.splice(oldIndex, 1);
+ updatedSubCategories.splice(newIndex, 0, movedCategory);
+
+ const updatedCategories = [...currentCategories];
+ updatedCategories[categoryIndex] = {
+ ...updatedCategories[categoryIndex],
+ subCategories: updatedSubCategories
+ };
+
+ return updatedCategories;
+ });
+ }
+ }
+
+ const handleCategoryMove = (categoryId: string) => {
+ setCategories(prevCategories => {
+ const newCategories = [...prevCategories];
+ let categoryToMove;
+ let targetCategory: CategoryType | undefined;
+
+ newCategories.forEach(category => {
+ if (category.subCategories) {
+ const subCategoryIndex = category.subCategories.findIndex(sub => sub.id === selectedCategory?.id);
+ if (subCategoryIndex !== -1) {
+ categoryToMove = category.subCategories[subCategoryIndex];
+ category.subCategories.splice(subCategoryIndex, 1);
+ }
+ }
+
+ if (category.id === categoryId) {
+ targetCategory = category;
+ }
+ });
+
+ if (!categoryToMove) {
+ return prevCategories;
+ }
+
+ if (categoryId === "top") {
+ newCategories.push(categoryToMove);
+ return newCategories;
+ }
+
+ if (!targetCategory) {
+ return prevCategories;
+ }
+
+ if (!targetCategory.subCategories) {
+ targetCategory.subCategories = [];
+ }
+
+ targetCategory.subCategories.push(categoryToMove);
+
+ return newCategories;
+ });
+ }
+
+ const submitCategoryChange = () => {
+ if (confirm("변경사항을 저장하시겠습니까?")) {
+ alert("저장되었습니다.");
+ navigate(0);
+ }
+ }
+
+ return (
+
+
+
+
+ {tabList.map((tab) =>
+ -
+
+
)}
+
+
+ {(selectedTab === "카테고리") &&
+
+
+
}
+ {(selectedTab === "게시글") &&
+
}
+ {(selectedTab === "프로필") &&
+
}
+
+ {showCategoryModal && }
+ {showCategoryAddModal && }
+
+ );
+}
+
+export default SettingPage;
\ No newline at end of file
diff --git a/src/router/root.tsx b/src/router/root.tsx
deleted file mode 100644
index b371c63..0000000
--- a/src/router/root.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import {createBrowserRouter} from "react-router-dom";
-import {Suspense} from "react";
-import {Loading, Login, Main} from "./page.tsx";
-
-const root = createBrowserRouter([
- {
- path: '',
- element:
- },
- {
- path: '/login',
- element:
- },
-]);
-
-export default root;
\ No newline at end of file
diff --git a/src/store.tsx b/src/store.tsx
index 9385c45..df571e2 100644
--- a/src/store.tsx
+++ b/src/store.tsx
@@ -1,5 +1,5 @@
import {configureStore} from "@reduxjs/toolkit";
-import loginSlice from "./slices/loginSlice.tsx";
+import loginSlice from "./common/slices/loginSlice.tsx";
const store = configureStore({
reducer: {