From 6873b4830b82e709a7af2f95882f355180a3dc71 Mon Sep 17 00:00:00 2001 From: Giwon Date: Wed, 2 Oct 2024 10:50:33 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EB=B0=9C=20+=20react-query=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 1 + .gitignore | 7 + package-lock.json | 123 +++++++++- package.json | 1 + src/App.jsx | 1 - .../display/dashboard/admin/AdminElement.jsx | 56 +++-- .../display/dashboard/home/board/Admin.jsx | 42 ++++ .../dashboard/home/board/Board.styled.js | 94 +++++++ .../dashboard/home/board/Executive.jsx | 37 +++ .../display/dashboard/home/board/History.jsx | 48 ++++ .../display/dashboard/home/board/Post.jsx | 38 +++ .../display/dashboard/home/board/User.jsx | 41 ++++ .../display/dashboard/home/board/index.js | 13 + .../display/dashboard/user/DeleteUser.jsx | 110 +++++++++ .../display/dashboard/user/DetailUser.jsx | 188 ++++++++++++++ .../display/dashboard/user/UpdatedUser.jsx | 68 ++++++ .../display/dashboard/user/UserElement.jsx | 127 ++++++++++ src/components/layouts/DashboardLayout.jsx | 62 +++-- src/components/navigation/DashboardNav.jsx | 7 +- src/pages/dashboard/Admin.jsx | 54 +--- src/pages/dashboard/History.jsx | 53 ++-- src/pages/dashboard/Home.jsx | 230 ++---------------- src/pages/dashboard/Home.styled.js | 71 +----- src/pages/dashboard/Users.jsx | 48 +++- src/pages/dashboard/Users.styled.js | 44 ++++ src/stores/dashboard/useAdmin.js | 16 -- src/stores/dashboard/useHome.js | 17 -- src/transitions/fade-slide.css | 2 +- src/utils/api.js | 2 +- src/utils/refineHistory.js | 27 ++ 30 files changed, 1177 insertions(+), 451 deletions(-) create mode 100644 .env.example create mode 100644 src/components/display/dashboard/home/board/Admin.jsx create mode 100644 src/components/display/dashboard/home/board/Board.styled.js create mode 100644 src/components/display/dashboard/home/board/Executive.jsx create mode 100644 src/components/display/dashboard/home/board/History.jsx create mode 100644 src/components/display/dashboard/home/board/Post.jsx create mode 100644 src/components/display/dashboard/home/board/User.jsx create mode 100644 src/components/display/dashboard/home/board/index.js create mode 100644 src/components/display/dashboard/user/DeleteUser.jsx create mode 100644 src/components/display/dashboard/user/DetailUser.jsx create mode 100644 src/components/display/dashboard/user/UpdatedUser.jsx create mode 100644 src/components/display/dashboard/user/UserElement.jsx create mode 100644 src/pages/dashboard/Users.styled.js delete mode 100644 src/stores/dashboard/useAdmin.js delete mode 100644 src/stores/dashboard/useHome.js create mode 100644 src/utils/refineHistory.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a5abc20 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_BACKEND_URL="URL_HERE \ No newline at end of file diff --git a/.gitignore b/.gitignore index a547bf3..350f1d9 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,10 @@ dist-ssr *.njsproj *.sln *.sw? + +# Environment files +.env +.env.local +.env.development +.env.* +!.env.example \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4375e7f..6ea962e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", "react-powerglitch": "^1.0.3", + "react-query": "^3.39.3", "react-router-dom": "^6.25.1", "react-transition-group": "^4.4.5", "styled-components": "^6.1.12", @@ -1743,18 +1744,41 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "node_modules/browserslist": { "version": "4.23.3", "dev": true, @@ -1892,7 +1916,6 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -2066,6 +2089,12 @@ "node": ">=0.4.0" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "dev": true, @@ -2687,7 +2716,6 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -2782,7 +2810,6 @@ }, "node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -2962,7 +2989,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -2971,7 +2997,6 @@ }, "node_modules/inherits": { "version": "2.0.4", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -3329,6 +3354,12 @@ "set-function-name": "^2.0.1" } }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -3470,6 +3501,22 @@ "yallist": "^3.0.2" } }, + "node_modules/match-sorter": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", + "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==", + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.52.0", "license": "MIT", @@ -3489,7 +3536,6 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -3503,6 +3549,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "license": "ISC", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.3.7", "funding": [ @@ -3627,9 +3682,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==", + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3723,7 +3783,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4026,6 +4085,32 @@ "react-dom": ">= 16.8.0" } }, + "node_modules/react-query": { + "version": "3.39.3", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", + "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.26.2", "license": "MIT", @@ -4113,6 +4198,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.5", "dev": true, @@ -4148,7 +4239,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -4663,6 +4753,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "dev": true, @@ -5340,7 +5440,6 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "dev": true, "license": "ISC" }, "node_modules/yallist": { diff --git a/package.json b/package.json index 766766f..5bd3438 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", "react-powerglitch": "^1.0.3", + "react-query": "^3.39.3", "react-router-dom": "^6.25.1", "react-transition-group": "^4.4.5", "styled-components": "^6.1.12", diff --git a/src/App.jsx b/src/App.jsx index 55cfa9a..77ebc0b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -22,7 +22,6 @@ import NewArticleEditor from './pages/NewArticleEditor'; import Login from './pages/Login'; import SignUp from './pages/SignUp'; import MyPage from './pages/MyPage'; -import Section6 from './pages/Section6'; export default function App() { // location.key을 통해 화면 전환 시 컴포넌트 충돌/중복 방지 용으로 사용됩니다. diff --git a/src/components/display/dashboard/admin/AdminElement.jsx b/src/components/display/dashboard/admin/AdminElement.jsx index 49f1820..d1077f1 100644 --- a/src/components/display/dashboard/admin/AdminElement.jsx +++ b/src/components/display/dashboard/admin/AdminElement.jsx @@ -1,5 +1,7 @@ import { useRef } from 'react'; +import { useQuery } from 'react-query'; import styled from 'styled-components'; +import PropTypes from 'prop-types'; import { Text } from '../../../typograph/Text'; @@ -77,11 +79,10 @@ export const AdminElement = ({ admin }) => { const { openAlert } = useAlert(); const { openConfirm, closeConfirm } = useConfirm(); - // 만약 admin이 없다면 반환 - if (!admin) { - console.warn('admin 객체를 받지 못했습니다.'); - return <>; - } + const { data, isLoading } = useQuery(`user-${admin.student_id}`, async () => { + const data = await API.GET(`/users/${admin.student_id}`); + return { ...admin, ...data }; + }); // 관리자 편집을 위한 Reference const refs = { @@ -90,17 +91,31 @@ export const AdminElement = ({ admin }) => { description: useRef(), }; + // 만약 admin이 없다면 반환 + if (!admin) { + console.warn('admin 객체를 받지 못했습니다.'); + return <>; + } + + if (isLoading) { + return ( + <> + + + ); + } + const profile_color = GenerateColorByString( - admin.student_id, - admin.generation, - admin.role, + data?.student_id, + data?.generation, + data?.role, ); // 관리자 요소를 눌렀을 때 이벤트 function onClick() { openConfirm({ title: '관리자 수정', - content: , + content: , onConfirm: () => UpdateAdmin(), confirm_label: '수정', cancel_label: '취소', @@ -161,15 +176,15 @@ export const AdminElement = ({ admin }) => { // 문제가 없다면 서버 요청 시작 showLoading({ message: '관리자 정보를 수정하는 중...' }); - API.PUT(`/admin/${admin.student_id}`, updated_admin) + API.PUT(`/admin/${data.student_id}`, updated_admin) .then((api_res) => { closeConfirm(); openAlert({ title: '관리자 정보 수정됨', content: ( ), onClose: () => window.location.reload(), @@ -190,11 +205,11 @@ export const AdminElement = ({ admin }) => { return ( {/* 프로필 사진이 없으면 Color Profile로 대체 */} - {!admin.profile_picture ? ( + {!data.profile_picture ? ( ) : ( { {/* 계정 정보 */} - {admin.name} ({admin.student_id}) + {data.name} ({data.student_id}) - {admin.role} · {admin.email} + {data.role} · {data.email} ); }; + +AdminElement.propTypes = { + admin: PropTypes.shape({ + student_id: PropTypes.number.isRequired, + generation: PropTypes.string.isRequired, + role: PropTypes.string.isRequired, + description: PropTypes.string, + }).isRequired, +}; diff --git a/src/components/display/dashboard/home/board/Admin.jsx b/src/components/display/dashboard/home/board/Admin.jsx new file mode 100644 index 0000000..8554a57 --- /dev/null +++ b/src/components/display/dashboard/home/board/Admin.jsx @@ -0,0 +1,42 @@ +import { useQuery } from 'react-query'; +import { useNavigate } from 'react-router-dom'; + +import { NumberDisplay } from '../../../NumberDisplay'; +import { + BoardButton, + BoardContainer, + BoardHeader, + SkeletonBoardContainer, +} from './Board.styled'; + +import { API } from '../../../../../utils/api'; + +export const Admin = () => { + const navigate = useNavigate(); + const { data, isLoading } = useQuery('admin', async () => { + const data = await API.GET('/admin'); + return data; + }); + + if (isLoading) { + return ; + } + + return ( + + 관리자 +
+ +
+ navigate('./admin')}> + 관리자 추가/제거로 이동 + +
+ ); +}; diff --git a/src/components/display/dashboard/home/board/Board.styled.js b/src/components/display/dashboard/home/board/Board.styled.js new file mode 100644 index 0000000..9f002bf --- /dev/null +++ b/src/components/display/dashboard/home/board/Board.styled.js @@ -0,0 +1,94 @@ +import styled from 'styled-components'; + +// 사용자 정의 컴포넌트 +import { Container } from '../../../../forms/Container'; +import { Span } from '../../../../typograph/Text'; + +export const BoardContainer = styled(Container).attrs({ + id: 'dashboard-home-container', +})` + @keyframes show { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + & > * { + animation: show 0.75s ease-in-out; + } + + width: 360px; + + margin: 0; + padding: 30px; + border-radius: 20px; + box-sizing: border-box; + overflow: hidden; + + display: flex; + flex-direction: column; + gap: 30px; +`; + +export const SkeletonBoardContainer = styled(BoardContainer)` + width: ${(props) => props.width}; + height: ${(props) => props.height}; + + background: linear-gradient( + 45deg, + var(--container-primary-background) 35%, + var(--container-secondary-background) 50%, + var(--container-primary-background) 65% + ); + background-size: 400% 400%; + animation: skeletonAnimation 1.5s infinite ease-in-out; + + @keyframes skeletonAnimation { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + } +`; + +export const BoardHeader = styled(Span).attrs({ + id: 'dashboard-container-header', + $size: 'l', + $weight: 'bold', +})``; + +/** + * 보드용 반투명 버튼 + */ +export const BoardButton = styled.button` + transition: background-color 0.2s ease-out; + + position: relative; + cursor: pointer; + overflow: hidden; + + width: ${(props) => props.width ?? 'fit-content'}; + height: ${(props) => props.height ?? 'fit-content'}; + padding: 12px 20px; + border-radius: 26px; + + border: none; + outline: none; + color: var(--primary-text-color); + background-color: var(--transparent-button-background); + font-weight: 700; + + box-sizing: border-box; + + &:hover { + background-color: var(--transparent-button-background-focus); + } +`; diff --git a/src/components/display/dashboard/home/board/Executive.jsx b/src/components/display/dashboard/home/board/Executive.jsx new file mode 100644 index 0000000..513ab0e --- /dev/null +++ b/src/components/display/dashboard/home/board/Executive.jsx @@ -0,0 +1,37 @@ +// import { useNavigate } from 'react-router-dom'; + +import { Text } from '../../../../typograph/Text'; +import { NumberDisplay } from '../../../NumberDisplay'; +import { BoardButton, BoardContainer, BoardHeader } from './Board.styled'; + +/* 임원진은 다음 개발에 진행합니다. */ +export const Executive = () => { + // const navigate = useNavigate(); + // const data = {}; + + return ( + + 임원진 + 준비중 + {/*
+ + +
+ navigate('./executive')}> + 임원진 추가/제거로 이동 + */} +
+ ); +}; diff --git a/src/components/display/dashboard/home/board/History.jsx b/src/components/display/dashboard/home/board/History.jsx new file mode 100644 index 0000000..a5f2c2c --- /dev/null +++ b/src/components/display/dashboard/home/board/History.jsx @@ -0,0 +1,48 @@ +import { useQuery } from 'react-query'; +import { useNavigate } from 'react-router-dom'; + +import { NumberDisplay } from '../../../NumberDisplay'; +import { + BoardButton, + BoardContainer, + BoardHeader, + SkeletonBoardContainer, +} from './Board.styled'; + +import { API } from '../../../../../utils/api'; +import { refineHistories } from '../../../../../utils/refineHistory'; + +export const History = () => { + const navigate = useNavigate(); + + const { data, isLoading } = useQuery('history', async () => { + const data = await API.GET('/histories'); + return data; + }); + + if (isLoading) { + return ; + } + + return ( + + 연혁 +
+ + +
+ navigate('./history')}> + 연혁 추가/제거로 이동 + +
+ ); +}; diff --git a/src/components/display/dashboard/home/board/Post.jsx b/src/components/display/dashboard/home/board/Post.jsx new file mode 100644 index 0000000..4c31433 --- /dev/null +++ b/src/components/display/dashboard/home/board/Post.jsx @@ -0,0 +1,38 @@ +// import { useNavigate } from 'react-router-dom'; + +import { Text } from '../../../../typograph/Text'; +import { NumberDisplay } from '../../../NumberDisplay'; +import { BoardButton, BoardContainer, BoardHeader } from './Board.styled'; + +export const Post = () => { + // const navigate = useNavigate(); + // const data = {}; + + return ( + + 소식지 + 준비중 + {/*
+ + +
+ navigate('/posts')}> + 소식지 페이지로 이동 + */} +
+ ); +}; diff --git a/src/components/display/dashboard/home/board/User.jsx b/src/components/display/dashboard/home/board/User.jsx new file mode 100644 index 0000000..82907a3 --- /dev/null +++ b/src/components/display/dashboard/home/board/User.jsx @@ -0,0 +1,41 @@ +import { useQuery } from 'react-query'; +import { useNavigate } from 'react-router-dom'; + +import { NumberDisplay } from '../../../NumberDisplay'; +import { + BoardButton, + BoardContainer, + BoardHeader, + SkeletonBoardContainer, +} from './Board.styled'; +import { API } from '../../../../../utils/api'; + +export const User = () => { + const navigate = useNavigate(); + const { data, isLoading } = useQuery('user', async () => { + const data = await API.GET('/users'); + return data; + }); + + if (isLoading) { + return ; + } + + return ( + + 회원 +
+ +
+ navigate('./users')}> + 회원 관리로 이동 + +
+ ); +}; diff --git a/src/components/display/dashboard/home/board/index.js b/src/components/display/dashboard/home/board/index.js new file mode 100644 index 0000000..1ab482d --- /dev/null +++ b/src/components/display/dashboard/home/board/index.js @@ -0,0 +1,13 @@ +import { Admin } from './Admin'; +import { Executive } from './Executive'; +import { History } from './History'; +import { Post } from './Post'; +import { User } from './User'; + +export const Board = { + Admin, + Executive, + History, + Post, + User, +}; diff --git a/src/components/display/dashboard/user/DeleteUser.jsx b/src/components/display/dashboard/user/DeleteUser.jsx new file mode 100644 index 0000000..e526b31 --- /dev/null +++ b/src/components/display/dashboard/user/DeleteUser.jsx @@ -0,0 +1,110 @@ +import { forwardRef } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +import { Span, Text } from '../../../typograph/Text'; +import { Container } from '../../../forms/Container'; + +import { ColorProfile } from '../../ColorProfile'; + +import { GenerateColorByString } from '../../../../utils/generateColor'; + +import { HoverToReveal } from '../../../forms/HoverToReveal'; +import { Input } from '../../../forms/Input'; + +const Preview = styled(Container)` + margin: 0; + margin-bottom: 30px; + + width: 100%; + padding: 30px; + border-radius: 20px; + + display: flex; + align-items: center; + gap: 20px; + + border: none; + background-color: var(--container-secondary-background); +`; + +const Description = styled(Span).attrs({ + $size: 'l', + $weight: 'bold', +})` + margin-bottom: 8px; +`; + +const Info = styled(Span).attrs({ + $size: 's', + $color: '--secondary-text-color', +})``; + +export const DeleteUser = forwardRef(({ user }, ref) => { + const profile_color = GenerateColorByString( + user.student_id, + user.generation, + user.major, + ); + + return ( + <> + {/* 수정할 타겟을 보여줌 */} + + {/* 프로필 사진이 없으면 Color Profile로 대체 */} + {!user.profile_picture ? ( + + ) : ( + + )} +
+ {user.name != '' ? user.name : '비어있음'} + {user.major} +
+
+
+ + 위 회원을 삭제하려면 학번을 입력하세요. + + + {user.student_id} + +
+ + + ); +}); +DeleteUser.displayName = 'DeleteUser'; +DeleteUser.propTypes = { + user: PropTypes.shape({ + student_id: PropTypes.number.isRequired, + generation: PropTypes.string.isRequired, + major: PropTypes.string.isRequired, + profile_picture: PropTypes.string, + name: PropTypes.string, + description: PropTypes.string, + }).isRequired, +}; diff --git a/src/components/display/dashboard/user/DetailUser.jsx b/src/components/display/dashboard/user/DetailUser.jsx new file mode 100644 index 0000000..dfe39c5 --- /dev/null +++ b/src/components/display/dashboard/user/DetailUser.jsx @@ -0,0 +1,188 @@ +import { useRef } from 'react'; +import styled from 'styled-components'; +import PropTypes from 'prop-types'; + +import { Span, Text } from '../../../typograph/Text'; +import { Button } from '../../../forms/Button'; +import { HintedInput } from '../../../forms/HintedInput'; +import { Container } from '../../../forms/Container'; + +import { ColorProfile } from '../../ColorProfile'; +import { DeleteUser } from './DeleteUser'; + +import { ErrorModal } from '../ErrorModal'; + +import { API } from '../../../../utils/api'; +import { GenerateColorByString } from '../../../../utils/generateColor'; +import useConfirm from '../../../../stores/useConfirm'; +import useAlert from '../../../../stores/useAlert'; +import useLoading from '../../../../stores/useLoading'; + +const Wrapper = styled.div` + margin: 40px 0; + + & > Button { + position: absolute; + left: 35px; + bottom: 35px; + } +`; + +const Preview = styled(Container)` + margin: 0; + margin-bottom: 30px; + + width: 100%; + padding: 30px; + border-radius: 20px; + + display: flex; + align-items: center; + gap: 20px; + + border: none; + background-color: var(--container-secondary-background); +`; + +const Name = styled(Span).attrs({ + $size: 'l', + $weight: 'bold', +})` + margin-bottom: 8px; +`; + +const Info = styled(Span).attrs({ + $size: 's', + $color: '--secondary-text-color', +})``; + +export const DetailUser = ({ user }) => { + const { showLoading, hideLoading } = useLoading(); + const { openAlert, closeAlert } = useAlert(); + const { openConfirm, closeConfirm } = useConfirm(); + + const refs = { + id_confirm: useRef(), + }; + + if (!user) { + console.error('user 객체가 없습니다.'); + return; + } + + const profile_color = GenerateColorByString( + user.student_id, + user.generation, + user.major, + ); + + // 관리자 삭제 이벤트 + function onDelete() { + closeAlert(); + openConfirm({ + title: '회원 삭제', + content: , + onConfirm: deleteUser, + confirm_label: '삭제', + confirm_color: 'var(--danger-color)', + cancel_label: '취소', + }); + } + + // 서버로 삭제 요청 + function deleteUser() { + // 게이트 1 - 입력한 학번이 삭제하려는 관리자의 학번과 일치하지 않을 떄 + if (refs.id_confirm.current.value != user.student_id.toString()) { + openAlert({ + title: '학번 불일치', + content: 삭제하려는 회원의 학번을 일치하게 적어주세요., + }); + return; + } + + // 서버로 요청 시도 + closeConfirm(); + showLoading({ message: '회원을 삭제하는 중...' }); + + API.DELETE(`/users/${user.student_id}`) + .then(() => { + openAlert({ + title: '회원 삭제 완료', + content: ( + + 회원 목록에서 {user.name}({user.student_id})를 삭제했어요. + + ), + onClose: () => window.location.reload(), + }); + }) + .catch((err) => { + // 오류 발생 시 안내 + openAlert({ + title: '통신 에러', + content: , + }); + }) + .finally(() => { + hideLoading(); + }); + } + + return ( + + {/* 수정할 타겟을 보여줌 */} + + {/* 프로필 사진이 없으면 Color Profile로 대체 */} + {!user.profile_picture ? ( + + ) : ( + + )} +
+ {user.name != '' ? user.name : '비어있음'} + + 학번 {user.student_id} +
+ 메일 {user.email} +
+
+
+ {/* 사용자가 수정하는 부분 */} +
+ + +
+ + +
+ ); +}; + +DetailUser.displayName = 'DetailUser'; +DetailUser.propTypes = { + user: PropTypes.shape({ + student_id: PropTypes.number.isRequired, + generation: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, + major: PropTypes.string.isRequired, + profile_picture: PropTypes.string, + }).isRequired, +}; diff --git a/src/components/display/dashboard/user/UpdatedUser.jsx b/src/components/display/dashboard/user/UpdatedUser.jsx new file mode 100644 index 0000000..bf5818c --- /dev/null +++ b/src/components/display/dashboard/user/UpdatedUser.jsx @@ -0,0 +1,68 @@ +import styled from 'styled-components'; + +import { Span } from '../../../typograph/Text'; + +import DownIcon from '../../../../assets/icons/down.svg'; + +const ID = styled(Span).attrs({ + $size: 's', + $weight: 'bold', + $color: '--secondary-text-color', +})` + margin-top: 30px; + margin-bottom: 6px; +`; + +const Description = styled(Span)` + margin-bottom: 30px; +`; + +const Display = styled.div` + width: 100%; + padding: 20px; + border-radius: 20px; + box-sizing: border-box; + + text-align: center; + background-color: var(--container-secondary-background); +`; + +const Info = styled(Span).attrs({ + $size: 's', + $weight: 'bold', + $color: '--secondary-text-color', +})` + margin-bottom: 8px; +`; + +const Content = styled(Span).attrs({ + $size: 'l', + $weight: 'extrabold', +})``; + +export const UpdatedUser = ({ current_admin, updated_admin }) => { + return ( + <> + Admin ID {updated_admin.student_id} + 관리자가 다음과 같이 수정되었습니다. + {/* 기존 관리자 */} + + + {current_admin.generation}ㆍ{current_admin.name}ㆍ{current_admin.role} + + {current_admin.description} + + {/* 화살표 */} +
+ +
+ {/* 변경된 관리자 */} + + + {updated_admin.generation}ㆍ{updated_admin.name}ㆍ{updated_admin.role} + + {updated_admin.description} + + + ); +}; diff --git a/src/components/display/dashboard/user/UserElement.jsx b/src/components/display/dashboard/user/UserElement.jsx new file mode 100644 index 0000000..03a4d81 --- /dev/null +++ b/src/components/display/dashboard/user/UserElement.jsx @@ -0,0 +1,127 @@ +import styled from 'styled-components'; +import PropTypes from 'prop-types'; + +import { Text } from '../../../typograph/Text'; + +import { GenerateColorByString } from '../../../../utils/generateColor'; +import { ColorProfile } from '../../ColorProfile'; + +import useAlert from '../../../../stores/useAlert'; +import { DetailUser } from './DetailUser'; + +const CardWrapper = styled.div` + transition: background-color 0.1s ease-in-out; + + min-width: 400px; + max-width: 460px; + padding: 20px; + box-sizing: border-box; + + display: flex; + align-items: center; + gap: 12px; + + flex-grow: 1; + flex-basis: 0; + + border-radius: 10px; + + cursor: pointer; + + &:hover { + background-color: var(--transparent-button-background); + } +`; + +const Info = styled.div` + display: flex; + flex-direction: column; + gap: 7px; +`; + +export const UserElementLoading = styled(CardWrapper)` + min-height: 88px; + + cursor: default; + + background: linear-gradient( + 45deg, + var(--container-primary-background) 35%, + var(--container-secondary-background) 50%, + var(--container-primary-background) 65% + ); + background-size: 400% 400%; + animation: skeletonAnimation 1.5s infinite ease-in-out; + + @keyframes skeletonAnimation { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + } +`; + +export const UserElement = ({ user }) => { + const { openAlert } = useAlert(); + + // 만약 user이 없다면 반환 + if (!user) { + console.warn('user 객체를 받지 못했습니다.'); + return <>; + } + + const profile_color = GenerateColorByString( + user.student_id, + user.generation, + user.major, + ); + + // 유저 요소를 눌렀을 때 이벤트 + function onClick() { + openAlert({ + title: '유저 정보', + content: , + }); + } + + return ( + + {/* 프로필 사진이 없으면 Color Profile로 대체 */} + {!user.profile_picture ? ( + + ) : ( + + )} + {/* 계정 정보 */} + + + {user.name} ({user.student_id}) + + + {user.major} + + + + ); +}; + +UserElement.propTypes = { + user: PropTypes.shape({ + student_id: PropTypes.number.isRequired, + generation: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, + major: PropTypes.string.isRequired, + profile_picture: PropTypes.string, + }).isRequired, +}; diff --git a/src/components/layouts/DashboardLayout.jsx b/src/components/layouts/DashboardLayout.jsx index 0a91cea..8e79fb2 100644 --- a/src/components/layouts/DashboardLayout.jsx +++ b/src/components/layouts/DashboardLayout.jsx @@ -1,7 +1,9 @@ import { createRef } from 'react'; import { useOutlet } from 'react-router-dom'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; +import { QueryClient, QueryClientProvider } from 'react-query'; import styled from 'styled-components'; +import PropTypes from 'prop-types'; // 사용자가 생성한 컴포넌트 및 JS파일 import import { DashboardNav } from '../navigation/DashboardNav'; @@ -49,8 +51,11 @@ const Content = styled(TransitionGroup).attrs({ } `; +// API 요청을 위한 QueryClient 생성 +const queryClient = new QueryClient(); + /** - * 대시보드 레이아웃 + * 대시보드 레이아 */ export const DashboardLayout = ({ location }) => { // Warning: findDOMNode is deprecated and will be remove 해제 @@ -62,28 +67,37 @@ export const DashboardLayout = ({ location }) => { const currentOutlet = useOutlet(); return ( - - - - - {/* 내비바 */} - - {/* 콘텐츠 */} - - {/* location.key로 랜덤한 index를 부여하여 화면 전환 시 컴포넌트 충돌이 없도록 예방합니다. */} - -
- {/* 전환 후 표시될 컴포넌트 */} - {currentOutlet} -
-
-
-
+ + + + + + {/* 내비바 */} + + {/* 콘텐츠 */} + + {/* location.key로 랜덤한 index를 부여하여 화면 전환 시 컴포넌트 충돌이 없도록 예방합니다. */} + +
+ {/* 전환 후 표시될 컴포넌트 */} + {currentOutlet} +
+
+
+
+
); }; + +DashboardLayout.propTypes = { + location: PropTypes.object.isRequired, +}; diff --git a/src/components/navigation/DashboardNav.jsx b/src/components/navigation/DashboardNav.jsx index 2e193e5..a8c9eb7 100644 --- a/src/components/navigation/DashboardNav.jsx +++ b/src/components/navigation/DashboardNav.jsx @@ -3,7 +3,7 @@ // 외부 라이브러리에서 import import styled from 'styled-components'; -import { useLocation, useNavigate, Link } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; // 사용자가 생성한 컴포넌트 및 JS파일 import import { Span } from '../typograph/Text'; @@ -220,9 +220,10 @@ export const DashboardNav = () => { } shortsName="연혁"> 연혁 추가/제거 - } shortsName="임원진"> + {/* 임원진은 다음 개발에 진행합니다. */} + {/* } shortsName="임원진"> 임원진 추가/제거 - + */} } shortsName="관리자"> 관리자 추가/제거 diff --git a/src/pages/dashboard/Admin.jsx b/src/pages/dashboard/Admin.jsx index 3507635..9d6f320 100644 --- a/src/pages/dashboard/Admin.jsx +++ b/src/pages/dashboard/Admin.jsx @@ -1,4 +1,5 @@ -import { useEffect, useRef, useState } from 'react'; +import { useRef } from 'react'; +import { useQuery } from 'react-query'; import { GENERATION_REGEX, STUDENT_ID_REGEX } from '../../utils/regex.js'; @@ -24,19 +25,20 @@ import { ErrorModal } from '../../components/display/dashboard/ErrorModal.jsx'; import { RefreshIcon } from '../../assets/icons'; import { API } from '../../utils/api.js'; -import useAdmin from '../../stores/dashboard/useAdmin.js'; import useAlert from '../../stores/useAlert.js'; import useConfirm from '../../stores/useConfirm.js'; import useLoading from '../../stores/useLoading.js'; export default function Admin() { - const [loading, setLoading] = useState(true); // 스켈레톤 컨테이너 로딩용 - const { admins, saveAdmins } = useAdmin(); - const { showLoading, hideLoading } = useLoading(); const { openConfirm, closeConfirm } = useConfirm(); const { openAlert } = useAlert(); + const { data, isLoading } = useQuery('admin', async () => { + const data = await API.GET('/admin'); + return data; + }); + const refs = { student_id: useRef(), generation: useRef(), @@ -44,42 +46,6 @@ export default function Admin() { description: useRef(), }; - // API로부터 데이터를 가져와 Zustand 상태를 업데이트합니다. - useEffect(() => { - // admin 데이터가 Store에 없는 경우 API 요청을 보냅니다. - if (admins.length === 0) { - // 1. 모든 관리자를 불러옵니다. - API.GET('/admin') - .then((adminRes) => { - // 2. 각 admin.student_id에 대해 추가 데이터를 가져옵니다. - const userRequests = adminRes.map((admin) => - API.GET(`/users/${admin.student_id}`).then((userRes) => ({ - ...userRes, // 추가로 가져온 사용자 데이터 - ...admin, // 기존 admin 데이터 - })), - ); - // 3. 모든 요청이 완료된 후 상태에 저장 - Promise.all(userRequests) - .then((adminsWithDetails) => { - saveAdmins(adminsWithDetails); // 병합된 데이터를 저장 - }) - .catch((error) => { - console.error('Error fetching data:', error); - }) - .finally(() => { - setLoading(false); - }); - }) - .catch((error) => { - setLoading(false); - console.error('Error fetching data:', error); - }); - } else { - setLoading(false); - console.info('이미 API 데이터가 있으므로 API 응답을 요청하지 않습니다.'); - } - }, [admins, saveAdmins]); - // 새로운 관리자 추가를 눌렀을 때 const onClick = () => { openConfirm({ @@ -139,7 +105,7 @@ export default function Admin() { // 위 if에 걸리지 않으면 서버 POST 요청 API.POST('/admin', new_admin) - .then((res) => { + .then(() => { openAlert({ title: '관리자 추가 성공', content: 페이지를 다시 불러올게요., @@ -180,9 +146,9 @@ export default function Admin() { {/* 관리자 리스트 */} - {loading + {isLoading ? [0, 1, 2, 3, 4].map((e, i) => ) - : admins.map((admin, index) => ( + : data.map((admin, index) => ( ))} diff --git a/src/pages/dashboard/History.jsx b/src/pages/dashboard/History.jsx index b31d8df..1cfce1a 100644 --- a/src/pages/dashboard/History.jsx +++ b/src/pages/dashboard/History.jsx @@ -1,4 +1,5 @@ -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { useQuery } from 'react-query'; // 사용자 정의 컴포넌트 import { Text } from '../../components/typograph/Text'; @@ -16,22 +17,33 @@ import { ErrorModal } from '../../components/display/dashboard/ErrorModal'; // 외부 훅 import { API } from '../../utils/api'; -import useHistory from '../../stores/dashboard/useHistory'; import useAlert from '../../stores/useAlert'; import useConfirm from '../../stores/useConfirm'; import useLoading from '../../stores/useLoading'; +import { refineHistories } from '../../utils/refineHistory'; // SVG 아이콘 import { AddIcon, RefreshIcon } from '../../assets/icons'; -import { Empty } from '../../components/display/dashboard/Empty'; export default function History() { const { openAlert } = useAlert(); const { openConfirm, closeConfirm } = useConfirm(); const { showLoading, hideLoading } = useLoading(); - const { saveHistory, histories } = useHistory(); // 불러온 데이터를 저장할 상태 - const [loading, setLoading] = useState(true); // 로딩 상태를 관리 + const [refined_history, setRefinedHistory] = useState({}); + + const { data, isLoading } = useQuery('history', async () => { + const data = await API.GET('/histories'); + return data; + }); + + // data가 로드되면 refinedHistory를 업데이트 + useEffect(() => { + if (data) { + const refined_data = refineHistories(data); + setRefinedHistory(refined_data); + } + }, [data, setRefinedHistory]); // 연혁 추가를 위한 Reference const refs = { @@ -40,29 +52,6 @@ export default function History() { description: useRef(), }; - useEffect(() => { - // 만약 이전에 받은 API 데이터가 없다면 API 요청 후 데이터를 store에 저장 - if (Object.keys(histories).length === 0) { - API.GET('/histories') - .then((api_res) => { - saveHistory(api_res); // API 데이터를 Zustand 상태에 반영 - }) - .catch((err) => { - // 오류 발생 시 안내 - openAlert({ - title: '통신 에러', - content: , - }); - }) - .finally(() => { - setLoading(false); - }); - } else { - setLoading(false); - console.info('이미 API 데이터가 있으므로 API 응답을 요청하지 않습니다.'); - } - }, []); - // 연혁 추가 버튼을 눌렀을 때 이벤트 function onAddHistory() { openConfirm({ @@ -128,21 +117,19 @@ export default function History() { - {loading ? ( + {isLoading ? ( <> - ) : Object.keys(histories).length === 0 ? ( - ) : ( - Object.keys(histories) + Object.keys(refined_history) .reverse() // history.year가 숫자이기 때문에 객체에서 자동으로 오름차순 정렬됩니다. 따라서 연도 key의 순서를 뒤집습니다. .map((year) => ( {year}년 - {histories[year].map((history, index) => ( + {refined_history[year].map((history, index) => ( ))} diff --git a/src/pages/dashboard/Home.jsx b/src/pages/dashboard/Home.jsx index b66c640..8a4aa52 100644 --- a/src/pages/dashboard/Home.jsx +++ b/src/pages/dashboard/Home.jsx @@ -1,223 +1,29 @@ -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; - // 사용자 정의 컴포넌트 -import { Text } from '../../components/typograph/Text'; import { Header } from './Dashboard.styled'; -import { - ContainerHeader, - HomeContainer, - Button, - BoardColumn, -} from './Home.styled'; -import { NumberDisplay } from '../../components/display/NumberDisplay'; -import HomeLoading from './Home.Loading'; - -import useHome from '../../stores/dashboard/useHome'; -import useAlert from '../../stores/useAlert'; - -const HistoryBoard = ({ name, data, navigate }) => { - return ( - - {name} -
- - -
- -
- ); -}; - -const ExecutiveBoard = ({ name, data, navigate }) => { - return ( - - {name} -
- - -
- -
- ); -}; +import { Column, Row } from './Home.styled'; -const AdminBoard = ({ name, data, navigate }) => { - return ( - - {name} -
- -
- -
- ); -}; - -const UserBoard = ({ name, data, navigate }) => { - return ( - - {name} -
- -
- -
- ); -}; - -const PostBoard = ({ name, data, navigate }) => { - return ( - - {name} -
- - -
- -
- ); -}; +// 홈에 표시될 보드들 +import { Board } from '../../components/display/dashboard/home/Board'; export default function Home() { - // 메뉴 변경마다 재렌더링을 막기 위해 useDashboardStore를 불러옵니다. - const { home, saveHome } = useHome(); - - // 다른 컴포넌트에서 URL 이동을 할 수 있도록 navigate를 넙깁니다. - const navigate = useNavigate(); - - const { openAlert } = useAlert(); - const [isLoading, setLoading] = useState(true); - - // 컴포넌트가 마운트되면 - useEffect(() => { - openAlert({ - title: '준비중', - content: ( - <> - Home은 아직 준비하고 있어요! - - ), - }); - - // 만약 이전에 받은 API 데이터가 없다면 API 요청 후 데이터를 store에 저장 - if (home == null) { - console.log('API 데이터가 없으므로 API 응답을 요청합니다.'); - - // API 요청하는 척 3초 Timeout을 겁니다. - setTimeout(() => { - setLoading(false); // 로딩 해제 - saveHome({ - history: { wholeCount: 16, yearCount: 10 }, - executive: { wholeCount: 36, displayCount: 5 }, - admin: { wholeCount: 5 }, - user: { wholeCount: 61 }, - post: { wholeCount: 102, perMonthCount: 2 }, - }); // API 응답 데이터 저장 - }, 3000); - } - // 이미 데이터가 있다면 요청 X - else { - console.log('이미 API 데이터가 있으므로 API 응답을 요청하지 않습니다.'); - setLoading(false); - } - }, []); - - return isLoading ? ( - - ) : ( + return ( <> {/* 유튜브 스튜디오의 배열을 따라갑니다. */}
- {/* 1차적으로 가로 방향 선 배치 후 2차적으로 세로 방향 후 배치 */} - - - - - - - - - - - + {/* 1차적으로 가로 방향 배치 후 2차적으로 세로 방향 배치 */} + + + + + + + + + + + + + ); } diff --git a/src/pages/dashboard/Home.styled.js b/src/pages/dashboard/Home.styled.js index c018065..806207d 100644 --- a/src/pages/dashboard/Home.styled.js +++ b/src/pages/dashboard/Home.styled.js @@ -1,76 +1,11 @@ import styled from 'styled-components'; -// 사용자 정의 컴포넌트 -import { Span } from '../../components/typograph/Text'; -import { Container } from '../../components/forms/Container'; - -/** - * 컨테이너 헤더 - */ -export const ContainerHeader = styled(Span).attrs({ - id: 'dashboard-container-header', - $size: 'l', - $weight: 'bold', -})``; - -export const HomeContainer = styled(Container).attrs({ - id: 'dashboard-home-container', -})` - @keyframes show { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } - } - - & > * { - animation: show 0.75s ease-in-out; - } - - width: 360px; - - margin: 0; - padding: 30px; - border-radius: 20px; - box-sizing: border-box; - overflow: hidden; - +export const Row = styled.div` display: flex; - flex-direction: column; - gap: 30px; -`; - -/** - * 대시보드용 반투명 버튼 - */ -export const Button = styled.button` - transition: background-color 0.2s ease-out; - - position: relative; - cursor: pointer; - overflow: hidden; - - width: ${(props) => props.$width ?? 'fit-content'}; - height: ${(props) => props.$height ?? 'fit-content'}; - padding: 12px 20px; - border-radius: 26px; - - border: none; - outline: none; - color: var(--primary-text-color); - background-color: var(--transparent-button-background); - font-weight: 700; - - box-sizing: border-box; - - &:hover { - background-color: var(--transparent-button-background-focus); - } + flex-wrap: wrap; /* 넘칠 경우 다음 줄로 이동 */ `; -export const BoardColumn = styled.div` +export const Column = styled.div` margin-right: 24px; margin-bottom: 24px; diff --git a/src/pages/dashboard/Users.jsx b/src/pages/dashboard/Users.jsx index b3e21db..92a699d 100644 --- a/src/pages/dashboard/Users.jsx +++ b/src/pages/dashboard/Users.jsx @@ -1,10 +1,52 @@ // 사용자 정의 컴포넌트 -import { Header } from './Dashboard.styled'; +import { Text } from '../../components/typograph/Text.jsx'; + +import { Header } from './Dashboard.styled.js'; +import { UserListContainer, UserHeader, UserList } from './Users.styled.js'; +import { + UserElement, + UserElementLoading, +} from '../../components/display/dashboard/user/UserElement.jsx'; + +import { API } from '../../utils/api.js'; +import { useQuery } from 'react-query'; + +export default function User() { + const { data, isLoading } = useQuery('user', async () => { + const data = await API.GET('/users'); + return data; + }); -export default function Users() { return ( <> -
회원 관리
+
관리자 추가/제거
+ + {/* 헤더 */} + +
+ + 일반 유저 목록 + + + 표시된 계정을 눌러서 계정을 삭제합니다. + +
+
+ {/* 유저 리스트 */} + + {!isLoading && data ? ( + data.map((user, index) => ) + ) : ( + <> + + + + + + + )} + +
); } diff --git a/src/pages/dashboard/Users.styled.js b/src/pages/dashboard/Users.styled.js new file mode 100644 index 0000000..071d483 --- /dev/null +++ b/src/pages/dashboard/Users.styled.js @@ -0,0 +1,44 @@ +import styled from 'styled-components'; + +// 사용자 정의 컴포넌트 +import { Container } from '../../components/forms/Container'; + +export const UserListContainer = styled(Container)` + display: flex; + flex-direction: column; + gap: 40px; + + width: 100%; + max-width: 1000px; + padding: 30px; + margin: 0; +`; + +export const UserHeader = styled.div` + display: flex; + justify-content: space-between; +`; + +export const UserList = styled.div` + display: flex; + flex-wrap: wrap; + gap: 16px; + flex-shrink: 1; +`; + +export const ControlBox = styled.div` + display: flex; + align-items: center; + gap: 16px; + + & > svg { + transition: fill 0.2s ease-in-out; + + fill: var(--secondary-text-color); + cursor: pointer; + } + + & > svg:hover { + fill: var(--primary-text-color); + } +`; diff --git a/src/stores/dashboard/useAdmin.js b/src/stores/dashboard/useAdmin.js deleted file mode 100644 index 7800c9a..0000000 --- a/src/stores/dashboard/useAdmin.js +++ /dev/null @@ -1,16 +0,0 @@ -// 대시보드 이동 시 이전에 요청한 API 응답 데이터를 저장합니다. -// 즉, API 재요청을 방지하는 용도입니다. -// 사용자가 최초 메뉴 접속 이후 새로운 데이터를 표시하려면 새로고침 등이 필요합니다. - -import { create } from 'zustand'; - -const useAdmin = create((set) => ({ - admins: [], - - // 외부 컴포넌트에서 API 응답을 받고 저장하는 용도입니다. - saveAdmins: (newList) => { - set({ admins: newList }); - }, -})); - -export default useAdmin; diff --git a/src/stores/dashboard/useHome.js b/src/stores/dashboard/useHome.js deleted file mode 100644 index 8e0c9f8..0000000 --- a/src/stores/dashboard/useHome.js +++ /dev/null @@ -1,17 +0,0 @@ -// 대시보드 이동 시 이전에 요청한 API 응답 데이터를 저장합니다. -// 즉, API 재요청을 방지하는 용도입니다. -// 사용자가 최초 메뉴 접속 이후 새로운 데이터를 표시하려면 새로고침 등이 필요합니다. - -import { create } from 'zustand'; - -const useHome = create((set) => ({ - // 대시보드 홈 데이터를 저장할 상태 - home: null, - - // setHome()으로 Home 데이터를 저장합니다. - saveHome: (newData) => { - set({ home: newData }); - }, -})); - -export default useHome; diff --git a/src/transitions/fade-slide.css b/src/transitions/fade-slide.css index 04af9df..ea7dda8 100644 --- a/src/transitions/fade-slide.css +++ b/src/transitions/fade-slide.css @@ -23,7 +23,7 @@ .fade-slide-exit-active { opacity: 0; transform: translateX(-70px); - transition: all 500ms cubic-bezier(0.27, 0.02, 0.26, 0.99); + transition: all 400ms cubic-bezier(0.27, 0.02, 0.26, 0.99); } .fade-slide-exit-done { diff --git a/src/utils/api.js b/src/utils/api.js index 0842899..ce417e6 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,4 +1,4 @@ -const API_URL = 'http://localhost:8000'; // API의 기본 URL을 정의합니다. +const API_URL = import.meta.env.VITE_BACKEND_URL; // API의 기본 URL을 .env에서 불러옵니다. // 기본적인 Fetch 요청 처리 함수 async function request(endpoint, method = 'GET', data = null, headers = {}) { diff --git a/src/utils/refineHistory.js b/src/utils/refineHistory.js new file mode 100644 index 0000000..933db94 --- /dev/null +++ b/src/utils/refineHistory.js @@ -0,0 +1,27 @@ +/** + * API 응답 형식에 맞는 history 오브젝트를 받으면 아래와 같이 재정렬 합니다. + * 1. 연도별로 구분 (2021,2023,2019...) + * 2. 연도 기준 내림차순 정렬 (2023,2021,2019...) + * 2. 연도 내부에서 월 기준 오름차순 정렬 (1, 2, 3, 4...) + * */ +export const refineHistories = (histories) => { + if (!histories) { + return {}; + } + + const groupedByYear = histories.reduce((acc, item) => { + const year = item.year; + if (!acc[year]) { + acc[year] = []; + } + acc[year].push(item); + return acc; + }, {}); + + // 각각 연도의 요소를 month순으로 정렬 + Object.keys(groupedByYear).forEach((year) => { + groupedByYear[year].sort((a, b) => a.month - b.month); + }); + + return groupedByYear; +}; From bb078948d8adcb793b5e1c51c1e630644a3a95ac Mon Sep 17 00:00:00 2001 From: Giwon Date: Sun, 6 Oct 2024 20:11:54 +0900 Subject: [PATCH 2/8] =?UTF-8?q?chore:=20QueryProvider=EB=A5=BC=20main.jsx?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layouts/DashboardLayout.jsx | 58 ++++++++++------------ src/main.jsx | 8 ++- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/components/layouts/DashboardLayout.jsx b/src/components/layouts/DashboardLayout.jsx index 8e79fb2..f8b30c9 100644 --- a/src/components/layouts/DashboardLayout.jsx +++ b/src/components/layouts/DashboardLayout.jsx @@ -1,7 +1,6 @@ import { createRef } from 'react'; import { useOutlet } from 'react-router-dom'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; -import { QueryClient, QueryClientProvider } from 'react-query'; import styled from 'styled-components'; import PropTypes from 'prop-types'; @@ -51,9 +50,6 @@ const Content = styled(TransitionGroup).attrs({ } `; -// API 요청을 위한 QueryClient 생성 -const queryClient = new QueryClient(); - /** * 대시보드 레이아 */ @@ -67,34 +63,32 @@ export const DashboardLayout = ({ location }) => { const currentOutlet = useOutlet(); return ( - - - - - - {/* 내비바 */} - - {/* 콘텐츠 */} - - {/* location.key로 랜덤한 index를 부여하여 화면 전환 시 컴포넌트 충돌이 없도록 예방합니다. */} - -
- {/* 전환 후 표시될 컴포넌트 */} - {currentOutlet} -
-
-
-
-
+ + + + + {/* 내비바 */} + + {/* 콘텐츠 */} + + {/* location.key로 랜덤한 index를 부여하여 화면 전환 시 컴포넌트 충돌이 없도록 예방합니다. */} + +
+ {/* 전환 후 표시될 컴포넌트 */} + {currentOutlet} +
+
+
+
); }; diff --git a/src/main.jsx b/src/main.jsx index 3f3fd14..83cf4a0 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,9 +1,15 @@ import { BrowserRouter } from 'react-router-dom'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; +import { QueryClient, QueryClientProvider } from 'react-query'; + +// API 요청을 위한 QueryClient 생성 +const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById('root')).render( - + + + , ); From 64adb3e87a117f456a54992a3be3d88849e99273 Mon Sep 17 00:00:00 2001 From: Giwon Date: Sun, 6 Oct 2024 20:12:21 +0900 Subject: [PATCH 3/8] =?UTF-8?q?chore:=20useQuery=20=EC=82=AC=EC=9A=A9=20+?= =?UTF-8?q?=20=EC=95=BD=EA=B0=84=EC=9D=98=20=EB=B0=98=EC=9D=91=ED=98=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/display/HistoryPreview.jsx | 224 ++++++---------------- 1 file changed, 62 insertions(+), 162 deletions(-) diff --git a/src/components/display/HistoryPreview.jsx b/src/components/display/HistoryPreview.jsx index ef70156..742fffb 100644 --- a/src/components/display/HistoryPreview.jsx +++ b/src/components/display/HistoryPreview.jsx @@ -1,19 +1,22 @@ import { createRef, useEffect, useState } from 'react'; import styled from 'styled-components'; +import PropTypes from 'prop-types'; import { Text } from '../typograph/Text'; -import useHistory from '../../stores/dashboard/useHistory'; import { API } from '../../utils/api'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import '../../transitions/fade-slide.css'; +import SAMPLE_HISTORIES from '../../utils/sampleHistories'; +import { useQuery } from 'react-query'; +import { refineHistories } from '../../utils/refineHistory'; const PreviewWrapper = styled.div` - width: 600px; + width: 500px; height: 300px; display: flex; - gap: 80px; + gap: min(5vw, 80px); `; const YearListWrapper = styled.div` @@ -25,19 +28,25 @@ const YearListWrapper = styled.div` const HistoryListWrapper = styled.div``; const HistoryElementWrapper = styled.div` + width: 100%; padding: 15px 20px; background-color: #ffffff10; border-radius: 10px; display: flex; flex-direction: column; + align-items: flex-start; gap: 6px; margin-bottom: 10px; + + & > span { + word-break: keep-all; + } `; const YearWrapper = styled.button` display: flex; align-items: center; - gap: 34px; + gap: 2vw; background-color: transparent; outline: none; border: none; @@ -64,7 +73,7 @@ const Year = styled.span` transition: color 0.2s ease-in-out; width: 63px; font-weight: 800; - font-size: 22px; + font-size: clamp(16px, 2vw, 24px); text-align: left; color: white; `; @@ -104,164 +113,53 @@ const HistoryElement = ({ history }) => { ); }; +HistoryElement.propTypes = { + history: PropTypes.shape({ + year: PropTypes.number.isRequired, + month: PropTypes.number.isRequired, + description: PropTypes.string.isRequired, + }).isRequired, +}; + export const HistoryPreview = () => { const nodeRef = createRef(null); const [display_year, setDisplayYear] = useState(); - const { saveHistory, histories } = useHistory(); // 불러온 데이터를 저장할 상태 + const [display_key, setDisplayKey] = useState(); + + const [sampleData] = useState(refineHistories(SAMPLE_HISTORIES)); + + const { data, isLoading, isError } = useQuery( + 'mainpage_history', + async () => { + let data = await API.GET('/histories'); + + // 만약 서버에 저장된 연혁이 없으면 기본 데이터 반환 + if (data.length > 0) { + data = refineHistories(data); + } else { + data = sampleData; + } + + return data; + }, + { + retry: false, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + refetchInterval: false, + }, + ); useEffect(() => { - // 만약 이전에 받은 API 데이터가 없다면 API 요청 후 데이터를 store에 저장 - if (Object.keys(histories).length === 0) { - API.GET('/histories') - .then((api_res) => { - const return_value = saveHistory(api_res); // API 데이터를 Zustand 상태에 반영 - setDisplayYear(Object.keys(return_value).reverse().at(0)); - console.log(api_res); - }) - .catch((err) => { - // 오류 발생 시 안내 - console.warn('History API 통신 실패. 기본 데이터를 사용합니다.', err); - const return_value = saveHistory([ - { - id: 0, - year: 1997, - month: 11, - description: '동아리 창립', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 1, - year: 2006, - month: 4, - description: '정보보호대학동아리엽학 KUCIS 소속', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 2, - year: 2008, - month: 8, - description: '한국정보보호진흥원 S/W 보안취약점 찾기 대회 우수상', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 3, - year: 2013, - month: 4, - description: '삼성소프트웨어프렌드쉽', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 4, - year: 2016, - month: 2, - description: '대경강원권 연합창업경진대회 최우수상', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 5, - year: 2016, - month: 4, - description: 'Naver D2 Campus 파트너 선정', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 6, - year: 2016, - month: 4, - description: '정보보호대학동아리연합 KUCIS 소속', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 7, - year: 2016, - month: 7, - description: 'KERPERENCE S/S 주최', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 8, - year: 2016, - month: 12, - description: 'KERPERENCE W/W 주최', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 9, - year: 2017, - month: 2, - description: 'KNU 창업 비즈니스 플랜 경진대회 대상', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 10, - year: 2017, - month: 4, - description: '정보보호대학 동아리 연합 KUCIS 소속', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 11, - year: 2018, - month: 4, - description: 'Naver D2 Campus 파트너 선정', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 12, - year: 2021, - month: 9, - description: '제2회 KOSPO 웹서비스 정보보안 경진대회 최우수상', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 13, - year: 2023, - month: 4, - description: 'HSpace 파트너십 체결', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 14, - year: 2024, - month: 4, - description: '전국사이버보안연합 CCA 소속', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - { - id: 15, - year: 2024, - month: 5, - description: '정보보호대학동아리연합 KUCIS 소속', - created_at: '2024-09-26T23:48:50.068140', - updated_at: '2024-09-26T23:48:50.068140', - }, - ]); - setDisplayYear(Object.keys(return_value).reverse().at(0)); - }) - .finally(() => { - console.log('History API 통신 종료'); - }); - } else { - console.info('이미 API 데이터가 있으므로 API 응답을 요청하지 않습니다.'); - } - }, [histories, saveHistory]); + const keys = Object.keys(data ?? sampleData).reverse(); + setDisplayYear(keys[0]); + setDisplayKey(keys.slice(0, 4)); + }, [data, sampleData]); - const display_key = Object.keys(histories).reverse().slice(0, 4); + if (isLoading) { + return
Loading...
; + } return ( @@ -286,11 +184,13 @@ export const HistoryPreview = () => { style={{ position: 'absolute' }} > - {histories[ - display_year ?? Object.keys(histories).reverse().at(0) - ]?.map((history, i) => ( - - ))} + {isError + ? sampleData[display_year]?.map((history, i) => ( + + )) + : data[display_year].map((history, i) => ( + + ))} From 0c42b72c9e8961c79f1dc496d884973c4702ddbe Mon Sep 17 00:00:00 2001 From: Giwon Date: Sun, 6 Oct 2024 20:12:52 +0900 Subject: [PATCH 4/8] =?UTF-8?q?chore:=20useQuery-GET=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../display/dashboard/home/board/Admin.jsx | 12 ++++-- .../display/dashboard/home/board/History.jsx | 12 ++++-- .../display/dashboard/home/board/User.jsx | 12 ++++-- src/pages/dashboard/Admin.jsx | 30 ++++++++----- src/pages/dashboard/Users.jsx | 42 +++++++++++-------- 5 files changed, 68 insertions(+), 40 deletions(-) diff --git a/src/components/display/dashboard/home/board/Admin.jsx b/src/components/display/dashboard/home/board/Admin.jsx index 8554a57..aed4762 100644 --- a/src/components/display/dashboard/home/board/Admin.jsx +++ b/src/components/display/dashboard/home/board/Admin.jsx @@ -13,10 +13,14 @@ import { API } from '../../../../../utils/api'; export const Admin = () => { const navigate = useNavigate(); - const { data, isLoading } = useQuery('admin', async () => { - const data = await API.GET('/admin'); - return data; - }); + const { data, isLoading } = useQuery( + 'admin', + async () => { + const data = await API.GET('/admin'); + return data; + }, + { retry: 2 }, + ); if (isLoading) { return ; diff --git a/src/components/display/dashboard/home/board/History.jsx b/src/components/display/dashboard/home/board/History.jsx index a5f2c2c..5d39ea0 100644 --- a/src/components/display/dashboard/home/board/History.jsx +++ b/src/components/display/dashboard/home/board/History.jsx @@ -15,10 +15,14 @@ import { refineHistories } from '../../../../../utils/refineHistory'; export const History = () => { const navigate = useNavigate(); - const { data, isLoading } = useQuery('history', async () => { - const data = await API.GET('/histories'); - return data; - }); + const { data, isLoading } = useQuery( + 'history', + async () => { + const data = await API.GET('/histories'); + return data; + }, + { retry: 2 }, + ); if (isLoading) { return ; diff --git a/src/components/display/dashboard/home/board/User.jsx b/src/components/display/dashboard/home/board/User.jsx index 82907a3..8936c26 100644 --- a/src/components/display/dashboard/home/board/User.jsx +++ b/src/components/display/dashboard/home/board/User.jsx @@ -12,10 +12,14 @@ import { API } from '../../../../../utils/api'; export const User = () => { const navigate = useNavigate(); - const { data, isLoading } = useQuery('user', async () => { - const data = await API.GET('/users'); - return data; - }); + const { data, isLoading } = useQuery( + 'user', + async () => { + const data = await API.GET('/users'); + return data; + }, + { retry: 2 }, + ); if (isLoading) { return ; diff --git a/src/pages/dashboard/Admin.jsx b/src/pages/dashboard/Admin.jsx index 9d6f320..d6b5d74 100644 --- a/src/pages/dashboard/Admin.jsx +++ b/src/pages/dashboard/Admin.jsx @@ -34,10 +34,14 @@ export default function Admin() { const { openConfirm, closeConfirm } = useConfirm(); const { openAlert } = useAlert(); - const { data, isLoading } = useQuery('admin', async () => { - const data = await API.GET('/admin'); - return data; - }); + const { data, isLoading, isError } = useQuery( + 'admin', + async () => { + const data = await API.GET('/admin'); + return data; + }, + { retry: 2 }, + ); const refs = { student_id: useRef(), @@ -145,13 +149,17 @@ export default function Admin() { {/* 관리자 리스트 */} - - {isLoading - ? [0, 1, 2, 3, 4].map((e, i) => ) - : data.map((admin, index) => ( - - ))} - + {isError ? ( + <> + ) : ( + + {isLoading + ? [0, 1, 2, 3, 4].map((e, i) => ) + : data.map((admin, index) => ( + + ))} + + )} ); diff --git a/src/pages/dashboard/Users.jsx b/src/pages/dashboard/Users.jsx index 92a699d..2a7d632 100644 --- a/src/pages/dashboard/Users.jsx +++ b/src/pages/dashboard/Users.jsx @@ -12,10 +12,14 @@ import { API } from '../../utils/api.js'; import { useQuery } from 'react-query'; export default function User() { - const { data, isLoading } = useQuery('user', async () => { - const data = await API.GET('/users'); - return data; - }); + const { data, isLoading, isError } = useQuery( + 'user', + async () => { + const data = await API.GET('/users'); + return data; + }, + { retry: 2 }, + ); return ( <> @@ -33,19 +37,23 @@ export default function User() { {/* 유저 리스트 */} - - {!isLoading && data ? ( - data.map((user, index) => ) - ) : ( - <> - - - - - - - )} - + {isError ? ( + <> + ) : ( + + {!isLoading && data ? ( + data.map((user, index) => ) + ) : ( + <> + + + + + + + )} + + )} ); From fad2198b80813fde1b1542cf812dc589c71c949f Mon Sep 17 00:00:00 2001 From: Giwon Date: Sun, 6 Oct 2024 20:14:01 +0900 Subject: [PATCH 5/8] =?UTF-8?q?chore:=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=A1=B0=EC=A0=95=20+=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Section6.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/Section6.jsx b/src/pages/Section6.jsx index 8f0f810..27e3674 100644 --- a/src/pages/Section6.jsx +++ b/src/pages/Section6.jsx @@ -38,7 +38,7 @@ const Content = styled.div` justify-content: space-around; align-items: center; gap: 10px; - @media (max-width: 900px) { + @media (max-width: 1000px) { & { flex-direction: column; /* 화면이 작아지면 세로 방향 */ justify-content: center; @@ -50,17 +50,22 @@ const Content = styled.div` `; const LeftContent = styled.div` + margin: 20px; + display: flex; flex-direction: column; gap: 80px; - @media (max-width: 900px) { + @media (max-width: 1000px) { & { gap: 30px; + align-items: center; } } `; -const RightContent = styled.div``; +const RightContent = styled.div` + margin: 20px; +`; const Title = styled(Span).attrs({ $weight: 'extrabold', From 213bd48967c238af2717d2e197c3302d5c43222a Mon Sep 17 00:00:00 2001 From: Giwon Date: Sun, 6 Oct 2024 20:15:02 +0900 Subject: [PATCH 6/8] =?UTF-8?q?remove:=20useQuery=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=BA=90=EC=8B=9C=EC=9A=A9=20store=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stores/dashboard/useHistory.js | 43 ------------------------------ 1 file changed, 43 deletions(-) delete mode 100644 src/stores/dashboard/useHistory.js diff --git a/src/stores/dashboard/useHistory.js b/src/stores/dashboard/useHistory.js deleted file mode 100644 index 4d02d2d..0000000 --- a/src/stores/dashboard/useHistory.js +++ /dev/null @@ -1,43 +0,0 @@ -// 대시보드 이동 시 이전에 요청한 API 응답 데이터를 저장합니다. -// 즉, API 재요청을 방지하는 용도입니다. -// 사용자가 최초 메뉴 접속 이후 새로운 데이터를 표시하려면 새로고침 등이 필요합니다. - -import { create } from 'zustand'; - -/** - * API 응답 형식에 맞는 history 오브젝트를 받으면 아래와 같이 재정렬 합니다. - * 1. 연도별로 구분 (2021,2023,2019...) - * 2. 연도 기준 내림차순 정렬 (2023,2021,2019...) - * 2. 연도 내부에서 월 기준 오름차순 정렬 (1, 2, 3, 4...) - * */ -const refineHistories = (histories) => { - const groupedByYear = histories.reduce((acc, item) => { - const year = item.year; - if (!acc[year]) { - acc[year] = []; - } - acc[year].push(item); - return acc; - }, {}); - - // 각각 연도의 요소를 month순으로 정렬 - Object.keys(groupedByYear).forEach((year) => { - groupedByYear[year].sort((a, b) => a.month - b.month); - }); - - return groupedByYear; -}; - -const useHistory = create((set) => ({ - histories: {}, - - // 외부 컴포넌트에서 API 응답을 받고 저장하는 용도입니다. - saveHistory: (api_res) => { - const refined_histories = refineHistories(api_res); - set({ histories: refined_histories }); - return refined_histories; - // console.log(refined_histories); - }, -})); - -export default useHistory; From 02e3634677dd5c35d18cfaa084cfbc73e22d427e Mon Sep 17 00:00:00 2001 From: Giwon Date: Sun, 6 Oct 2024 20:15:58 +0900 Subject: [PATCH 7/8] =?UTF-8?q?test:=20=EC=97=B0=ED=98=81=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=8B=A4=ED=8C=A8=EC=8B=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=A0=20=EC=83=98=ED=94=8C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/sampleHistories.js | 132 +++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/utils/sampleHistories.js diff --git a/src/utils/sampleHistories.js b/src/utils/sampleHistories.js new file mode 100644 index 0000000..66c07eb --- /dev/null +++ b/src/utils/sampleHistories.js @@ -0,0 +1,132 @@ +const SAMPLE_HISTORIES = [ + { + id: 0, + year: 1997, + month: 11, + description: '동아리 창립', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 1, + year: 2006, + month: 4, + description: '정보보호대학동아리엽학 KUCIS 소속', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 2, + year: 2008, + month: 8, + description: '한국정보보호진흥원 S/W 보안취약점 찾기 대회 우수상', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 3, + year: 2013, + month: 4, + description: '삼성소프트웨어프렌드쉽', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 4, + year: 2016, + month: 2, + description: '대경강원권 연합창업경진대회 최우수상', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 5, + year: 2016, + month: 4, + description: 'Naver D2 Campus 파트너 선정', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 6, + year: 2016, + month: 4, + description: '정보보호대학동아리연합 KUCIS 소속', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 7, + year: 2016, + month: 7, + description: 'KERPERENCE S/S 주최', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 8, + year: 2016, + month: 12, + description: 'KERPERENCE W/W 주최', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 9, + year: 2017, + month: 2, + description: 'KNU 창업 비즈니스 플랜 경진대회 대상', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 10, + year: 2017, + month: 4, + description: '정보보호대학 동아리 연합 KUCIS 소속', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 11, + year: 2018, + month: 4, + description: 'Naver D2 Campus 파트너 선정', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 12, + year: 2021, + month: 9, + description: '제2회 KOSPO 웹서비스 정보보안 경진대회 최우수상', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 13, + year: 2023, + month: 4, + description: 'HSpace 파트너십 체결', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 14, + year: 2024, + month: 4, + description: '전국사이버보안연합 CCA 소속', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, + { + id: 15, + year: 2024, + month: 5, + description: '정보보호대학동아리연합 KUCIS 소속', + created_at: '2024-09-26T23:48:50.068140', + updated_at: '2024-09-26T23:48:50.068140', + }, +]; + +export default SAMPLE_HISTORIES; From 492c072583351203328799b6dd60a35e68d0dc24 Mon Sep 17 00:00:00 2001 From: Giwon Date: Sun, 6 Oct 2024 21:16:17 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=EB=8C=80=EC=86=8C=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/dashboard/Home.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/dashboard/Home.jsx b/src/pages/dashboard/Home.jsx index 8a4aa52..a439631 100644 --- a/src/pages/dashboard/Home.jsx +++ b/src/pages/dashboard/Home.jsx @@ -3,7 +3,7 @@ import { Header } from './Dashboard.styled'; import { Column, Row } from './Home.styled'; // 홈에 표시될 보드들 -import { Board } from '../../components/display/dashboard/home/Board'; +import { Board } from '../../components/display/dashboard/home/board'; export default function Home() { return (