diff --git a/package-lock.json b/package-lock.json index 76c0f62..ad840c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "prettier": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.5", "react": "^18.3.1", + "react-cookie": "^7.2.0", "react-dom": "^18.3.1", "react-icons": "^5.2.1", "react-markdown": "^9.0.1", @@ -11334,6 +11335,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", @@ -11456,6 +11462,15 @@ "@types/unist": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -18767,6 +18782,19 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/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==" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -27011,6 +27039,19 @@ "react": "^16.3.0 || ^17.0.1 || ^18.0.0" } }, + "node_modules/react-cookie": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.0.tgz", + "integrity": "sha512-mqhPERUyfOljq5yJ4woDFI33bjEtigsl8JDJdPPeNhr0eSVZmBc/2Vdf8mFxOUktQxhxTR1T+uF0/FRTZyBEgw==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.5", + "hoist-non-react-statics": "^3.3.2", + "universal-cookie": "^7.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -30357,6 +30398,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universal-cookie": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.2.0.tgz", + "integrity": "sha512-PvcyflJAYACJKr28HABxkGemML5vafHmiL4ICe3e+BEKXRMt0GaFLZhAwgv637kFFnnfiSJ8e6jknrKkMrU+PQ==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/package.json b/package.json index 68cce1f..a6b662e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "prettier": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.5", "react": "^18.3.1", + "react-cookie": "^7.2.0", "react-dom": "^18.3.1", "react-icons": "^5.2.1", "react-markdown": "^9.0.1", diff --git a/src/api/admin-check-question-answer.ts b/src/api/admin-check-question-answer.ts new file mode 100644 index 0000000..629c174 --- /dev/null +++ b/src/api/admin-check-question-answer.ts @@ -0,0 +1,24 @@ +import { server_axiosInstance } from '../utils/axios'; +import { getCookie } from '../utils/cookies'; + +interface AdminCheckQuestionAnswerProps { + questionId: number; + check: boolean; +} +export async function AdminCheckQuestionAnswer({ questionId, check }: AdminCheckQuestionAnswerProps) { + const data = { + questionId, + check, + }; + try { + const response = await server_axiosInstance.post('/api/admin/questions/check', JSON.stringify(data), { + headers: { + Authorization: `Bearer ${getCookie('accessToken')}`, + 'Content-Type': 'application/json', + }, + }); + return response.data; + } catch (error: any) { + throw new Error('checked failed'); + } +} diff --git a/src/api/admin-edit-answer.ts b/src/api/admin-edit-answer.ts new file mode 100644 index 0000000..6d96676 --- /dev/null +++ b/src/api/admin-edit-answer.ts @@ -0,0 +1,22 @@ +import { server_axiosInstance } from '../utils/axios'; +import { getCookie } from '../utils/cookies'; + +export async function AdminEditAnswer(id: number, content: string): Promise { + const data = { + id, + content, + }; + + try { + const response = await server_axiosInstance.post('/api/admin/answers', JSON.stringify(data), { + headers: { + Authorization: `Bearer ${getCookie('accessToken')}`, + 'Content-Type': 'application/json', + }, + }); + console.log(response.data); + return response.data; + } catch (error: any) { + throw new Error('Post Failed'); + } +} diff --git a/src/api/admin-login.ts b/src/api/admin-login.ts index 64b34f1..0c6c44c 100644 --- a/src/api/admin-login.ts +++ b/src/api/admin-login.ts @@ -1,34 +1,30 @@ -import axios from 'axios'; -import useSWR, { mutate } from 'swr'; +import { server_axiosInstance } from '../utils/axios'; +import { getCookie, removeCookie, setCookie } from '../utils/cookies'; -interface LoginResponse { - token: string; - user: { - email: string; +export async function AdminLogin(email: string, password: string) { + const data = { + email, + password, }; -} -const fetcher = (url: string) => axios.get(url).then((res) => res.data); + try { + if (getCookie('accessToken')) { + removeCookie('accessToken'); + removeCookie('refreshToken'); + } -export async function login(email: string, password: string): Promise { - const response = await axios.post('경로변경넣어야됌', { email, password }, { withCredentials: true }); - return response.data; -} + const response = await server_axiosInstance.post('/api/auth/sign-in', data); + const accessToken: string = response.headers['authorization']; + const refreshToken: string = response.headers['authorization-refresh']; -export function useUser() { - const { data, error } = useSWR('경로넣어야행', fetcher); - return { - user: data, - isLoading: !error && !data, - isError: error, - }; + setCookie('accessToken', accessToken.split(' ')[1], { path: '/admin', secure: true, sameSite: 'lax' }); + setCookie('refreshToken', refreshToken.split(' ')[1], { path: '/admin', secure: true, sameSite: 'lax' }); + } catch (error: any) { + throw new Error('Login Failed - Server network failed'); + } } -export async function handleLogin(email: string, password: string): Promise { - try { - const userData = await login(email, password); - mutate('경로헤헿콩', userData, false); - } catch (err: any) { - throw new Error(err.message); - } +export function AdminLogout() { + removeCookie('accessToken', { path: '/admin', secure: true, sameSite: 'lax' }); + removeCookie('refreshToken', { path: '/admin', secure: true, sameSite: 'lax' }); } diff --git a/src/api/admin-question-check.ts b/src/api/admin-question-check.ts new file mode 100644 index 0000000..a25185b --- /dev/null +++ b/src/api/admin-question-check.ts @@ -0,0 +1,20 @@ +import { server_axiosInstance } from '../utils/axios'; + +interface AdminQuestionCheckProps { + type: string; + category?: string; +} + +export async function adminQuestionCheck({ type, category }: AdminQuestionCheckProps) { + try { + const response = await server_axiosInstance.get('/api/questions', { + params: { + type, + category, + }, + }); + return response.data; + } catch (error: any) { + throw new Error(`Upload failed: ${error.message}`); + } +} diff --git a/src/api/admin-retrieve-file.ts b/src/api/admin-retrieve-file.ts new file mode 100644 index 0000000..a1b377c --- /dev/null +++ b/src/api/admin-retrieve-file.ts @@ -0,0 +1,20 @@ +import { llm_axiosInstance } from '../utils/axios'; + +interface AdminRetrieveFileProps { + type?: string; + category?: string; +} + +export async function adminRetrieveFile({ type, category }: AdminRetrieveFileProps) { + try { + const response = await llm_axiosInstance.get('/retrieve_documents_by_type_and_category', { + params: { + type, + category, + }, + }); + return response.data; + } catch (error: any) { + throw new Error(`Upload failed: ${error.message}`); + } +} diff --git a/src/index.tsx b/src/index.tsx index 1a58c8e..234a8af 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,9 +3,14 @@ import App from './App'; import ReactDOM from 'react-dom/client'; import reportWebVitals from './reportWebVitals'; import './index.css'; +import { CookiesProvider } from 'react-cookie'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); -root.render(); +root.render( + + + , +); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) diff --git a/src/routes/admin-routes.tsx b/src/routes/admin-routes.tsx index c030e8f..e1cfca7 100644 --- a/src/routes/admin-routes.tsx +++ b/src/routes/admin-routes.tsx @@ -3,16 +3,17 @@ import { Route, Routes } from 'react-router-dom'; import PrivateRoute from './private-route'; import FileList from '../ui/pages/admin/file-list'; import Login from '../ui/pages/admin/login'; -import QuestionCheck from '../ui/pages/admin/question-check'; import AdminLayout from '../ui/components/admin/Layout/admin-layout'; - +import { getCookie } from '../utils/cookies'; +import QuestionCheck from '../ui/pages/admin/question-check'; const AdminRoutes: React.FC = () => { - const token = sessionStorage.getItem('Authorization'); + const token = getCookie('accessToken'); return ( } /> } authenticated={token} />}> } /> + } /> ); diff --git a/src/routes/app-routes.tsx b/src/routes/app-routes.tsx index 8b6ce2b..8c1de84 100644 --- a/src/routes/app-routes.tsx +++ b/src/routes/app-routes.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; import AdminRoutes from './admin-routes'; -import PageTest from '../ui/pages/page-test'; import MaruEgg from '../ui/pages/maru-egg'; const AppRoutes: React.FC = () => { @@ -9,7 +8,6 @@ const AppRoutes: React.FC = () => { } /> } /> - } /> ); }; diff --git a/src/routes/private-route.tsx b/src/routes/private-route.tsx index f2d49d8..445a8c1 100644 --- a/src/routes/private-route.tsx +++ b/src/routes/private-route.tsx @@ -7,7 +7,7 @@ export interface PrivateRouteProps { } const PrivateRoute = ({ authenticated, component }: PrivateRouteProps) => { - return authenticated ? component : ; + return authenticated ? component : ; }; export default PrivateRoute; diff --git a/src/ui/components/admin/Layout/admin-layout.tsx b/src/ui/components/admin/Layout/admin-layout.tsx index 79dd66a..277d590 100644 --- a/src/ui/components/admin/Layout/admin-layout.tsx +++ b/src/ui/components/admin/Layout/admin-layout.tsx @@ -1,19 +1,27 @@ import React, { useEffect, useState } from 'react'; -import { Breadcrumb, Layout, Menu } from 'antd'; -import { Outlet, useLocation } from 'react-router-dom'; +import { Breadcrumb, Button, Layout, Menu } from 'antd'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import SlideMenu from '../slide-menu/slide-menu'; import { Header } from 'antd/es/layout/layout'; +import { AdminLogout } from '../../../../api/admin-login'; +import { removeCookie } from '../../../../utils/cookies'; const { Sider, Content, Footer } = Layout; const AdminLayout: React.FC = () => { const { pathname } = useLocation(); + const navigate = useNavigate(); useEffect(() => { pathname.split('/').map((a, i) => { console.log(pathname.split('/')[i]); }); }); + const handleLogout = () => { + AdminLogout(); + navigate('/admin/login'); + }; + const breadcrumpItem1 = pathname.split('/')[1]; const breadcrumpItem2 = pathname.split('/')[2]; @@ -24,8 +32,11 @@ const AdminLayout: React.FC = () => { -
+
+
diff --git a/src/ui/components/admin/modal/edit-modal.tsx b/src/ui/components/admin/modal/edit-modal.tsx new file mode 100644 index 0000000..c4b27b7 --- /dev/null +++ b/src/ui/components/admin/modal/edit-modal.tsx @@ -0,0 +1,106 @@ +import { Button, Modal } from 'antd'; +import TextArea from 'antd/es/input/TextArea'; +import React, { useState } from 'react'; +import { AdminEditAnswer } from '../../../../api/admin-edit-answer'; +import { AdminCheckQuestionAnswer } from '../../../../api/admin-check-question-answer'; + +interface CustomModalProps { + open: boolean; + setOpen: React.Dispatch>; + modalTitle: string; + modalContent: string; + modalContentId: number; + questionId: number; + isChecked: boolean; +} + +const EditModal = ({ + open, + setOpen, + modalTitle, + modalContent, + modalContentId, + questionId, + isChecked, +}: CustomModalProps) => { + const [editStatus, setEditStatus] = useState(false); + const [loading, setLoading] = useState(false); + const [content, setContent] = useState(modalContent); + + const executeWithLoading = async (action: () => Promise) => { + setLoading(true); + try { + await action(); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + const handleEditSubmit = async () => { + try { + await executeWithLoading(async () => { + await AdminEditAnswer(modalContentId, content); + await AdminCheckQuestionAnswer({ questionId, check: true }); + }); + } catch (err) { + setEditStatus(false); + console.error(err); + } + }; + + const handleCheckToggle = async () => { + await executeWithLoading(async () => { + await AdminCheckQuestionAnswer({ questionId, check: !isChecked }); + }); + }; + + const handleClose = () => { + setOpen(false); + }; + + const enableEditMode = () => { + setEditStatus(true); + }; + + const handleChange = (e: React.ChangeEvent) => { + setContent(e.target.value); + }; + + return ( + + 닫기 + , + !isChecked ? ( + + ) : ( + + ), + + editStatus ? ( + + ) : ( + + ), + ]} + > + {editStatus ?