diff --git a/.pnp.cjs b/.pnp.cjs index 90b9b73..7b74fcd 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -2398,6 +2398,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@kurkle/color", [\ + ["npm:0.3.2", {\ + "packageLocation": "./.yarn/cache/@kurkle-color-npm-0.3.2-98f2086013-a9e8e3e35d.zip/node_modules/@kurkle/color/",\ + "packageDependencies": [\ + ["@kurkle/color", "npm:0.3.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@lukeed/csprng", [\ ["npm:1.1.0", {\ "packageLocation": "./.yarn/cache/@lukeed-csprng-npm-1.1.0-d28ed78cc2-5d6dcf478a.zip/node_modules/@lukeed/csprng/",\ @@ -4482,6 +4491,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/howler", [\ + ["npm:2.2.11", {\ + "packageLocation": "./.yarn/cache/@types-howler-npm-2.2.11-dd9e5eb3f8-0a6b6d3585.zip/node_modules/@types/howler/",\ + "packageDependencies": [\ + ["@types/howler", "npm:2.2.11"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/http-cache-semantics", [\ ["npm:4.0.4", {\ "packageLocation": "./.yarn/cache/@types-http-cache-semantics-npm-4.0.4-6d4f413ddd-51b72568b4.zip/node_modules/@types/http-cache-semantics/",\ @@ -4770,6 +4788,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/react-howler", [\ + ["npm:5.2.3", {\ + "packageLocation": "./.yarn/cache/@types-react-howler-npm-5.2.3-e13418b6ae-8c5ddded67.zip/node_modules/@types/react-howler/",\ + "packageDependencies": [\ + ["@types/react-howler", "npm:5.2.3"],\ + ["@types/howler", "npm:2.2.11"],\ + ["@types/react", "npm:18.2.37"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/react-reconciler", [\ ["npm:0.26.7", {\ "packageLocation": "./.yarn/cache/@types-react-reconciler-npm-0.26.7-41be099833-ed01a9cdc8.zip/node_modules/@types/react-reconciler/",\ @@ -5934,10 +5963,12 @@ const RAW_RUNTIME_STATE = ["@typescript-eslint/eslint-plugin", "virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:6.13.1"],\ ["@typescript-eslint/parser", "virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:6.13.1"],\ ["@vitejs/plugin-react", "virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:4.2.0"],\ + ["chart.js", "npm:4.4.1"],\ ["eslint", "npm:8.55.0"],\ ["eslint-plugin-react-hooks", "virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:4.6.0"],\ ["eslint-plugin-react-refresh", "virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:0.4.5"],\ ["react", "npm:18.2.0"],\ + ["react-chartjs-2", "virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:5.2.0"],\ ["react-dom", "virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:18.2.0"],\ ["react-router-dom", "virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:6.20.1"],\ ["typescript", "patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441"],\ @@ -6881,6 +6912,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["chart.js", [\ + ["npm:4.4.1", {\ + "packageLocation": "./.yarn/cache/chart.js-npm-4.4.1-bea8f3ff67-ef0cd10118.zip/node_modules/chart.js/",\ + "packageDependencies": [\ + ["chart.js", "npm:4.4.1"],\ + ["@kurkle/color", "npm:0.3.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["chokidar", [\ ["npm:3.5.3", {\ "packageLocation": "./.yarn/cache/chokidar-npm-3.5.3-c5f9b0a56a-1076953093.zip/node_modules/chokidar/",\ @@ -7040,6 +7081,7 @@ const RAW_RUNTIME_STATE = ["@types/react", "npm:18.2.37"],\ ["@types/react-color", "npm:3.0.10"],\ ["@types/react-dom", "npm:18.2.15"],\ + ["@types/react-howler", "npm:5.2.3"],\ ["@types/react-router-dom", "npm:5.3.3"],\ ["@types/three", "npm:0.158.2"],\ ["@typescript-eslint/eslint-plugin", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:6.10.0"],\ @@ -7056,6 +7098,8 @@ const RAW_RUNTIME_STATE = ["react", "npm:18.2.0"],\ ["react-color", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:2.19.3"],\ ["react-dom", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:18.2.0"],\ + ["react-full-screen", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:1.1.1"],\ + ["react-howler", "npm:5.2.0"],\ ["react-markdown", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:9.0.1"],\ ["react-router-dom", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:6.18.0"],\ ["remark-gfm", "npm:4.0.0"],\ @@ -9080,6 +9124,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["fscreen", [\ + ["npm:1.2.0", {\ + "packageLocation": "./.yarn/cache/fscreen-npm-1.2.0-cbeb194fbc-1a5cb44446.zip/node_modules/fscreen/",\ + "packageDependencies": [\ + ["fscreen", "npm:1.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["fsevents", [\ ["patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1", {\ "packageLocation": "./.yarn/unplugged/fsevents-patch-6b67494872/node_modules/fsevents/",\ @@ -9497,6 +9550,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["howler", [\ + ["npm:2.2.4", {\ + "packageLocation": "./.yarn/cache/howler-npm-2.2.4-689ccf19b1-74a295f56f.zip/node_modules/howler/",\ + "packageDependencies": [\ + ["howler", "npm:2.2.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["html-escaper", [\ ["npm:2.0.2", {\ "packageLocation": "./.yarn/cache/html-escaper-npm-2.0.2-38e51ef294-208e8a12de.zip/node_modules/html-escaper/",\ @@ -13518,6 +13580,32 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["react-chartjs-2", [\ + ["npm:5.2.0", {\ + "packageLocation": "./.yarn/cache/react-chartjs-2-npm-5.2.0-03632f5179-437e443a26.zip/node_modules/react-chartjs-2/",\ + "packageDependencies": [\ + ["react-chartjs-2", "npm:5.2.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:5.2.0", {\ + "packageLocation": "./.yarn/__virtual__/react-chartjs-2-virtual-2e5b9b33bb/0/cache/react-chartjs-2-npm-5.2.0-03632f5179-437e443a26.zip/node_modules/react-chartjs-2/",\ + "packageDependencies": [\ + ["react-chartjs-2", "virtual:94de940a12aebd19f10e0a5b5702e435ff38d3f2a1b8621121e35f085410d4e466a0d0951ce6ee0822f9dc2cc191756fd20ef02eac83de5ee242939c7842ffa5#npm:5.2.0"],\ + ["@types/chart.js", null],\ + ["@types/react", "npm:18.2.41"],\ + ["chart.js", "npm:4.4.1"],\ + ["react", "npm:18.2.0"]\ + ],\ + "packagePeers": [\ + "@types/chart.js",\ + "@types/react",\ + "chart.js",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react-color", [\ ["npm:2.19.3", {\ "packageLocation": "./.yarn/cache/react-color-npm-2.19.3-d63f96d772-6f9fce9ff3.zip/node_modules/react-color/",\ @@ -13660,6 +13748,40 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["react-full-screen", [\ + ["npm:1.1.1", {\ + "packageLocation": "./.yarn/cache/react-full-screen-npm-1.1.1-4d2e312e1b-02c6034c6a.zip/node_modules/react-full-screen/",\ + "packageDependencies": [\ + ["react-full-screen", "npm:1.1.1"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:1.1.1", {\ + "packageLocation": "./.yarn/__virtual__/react-full-screen-virtual-9bbf69d77c/0/cache/react-full-screen-npm-1.1.1-4d2e312e1b-02c6034c6a.zip/node_modules/react-full-screen/",\ + "packageDependencies": [\ + ["react-full-screen", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:1.1.1"],\ + ["@types/react", "npm:18.2.37"],\ + ["fscreen", "npm:1.2.0"],\ + ["react", "npm:18.2.0"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["react-howler", [\ + ["npm:5.2.0", {\ + "packageLocation": "./.yarn/cache/react-howler-npm-5.2.0-211ee607c8-b4b4572851.zip/node_modules/react-howler/",\ + "packageDependencies": [\ + ["react-howler", "npm:5.2.0"],\ + ["howler", "npm:2.2.4"],\ + ["prop-types", "npm:15.8.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react-is", [\ ["npm:16.13.1", {\ "packageLocation": "./.yarn/cache/react-is-npm-16.13.1-a9b9382b4f-33977da7a5.zip/node_modules/react-is/",\ diff --git a/.yarn/cache/@kurkle-color-npm-0.3.2-98f2086013-a9e8e3e35d.zip b/.yarn/cache/@kurkle-color-npm-0.3.2-98f2086013-a9e8e3e35d.zip new file mode 100644 index 0000000..723d786 Binary files /dev/null and b/.yarn/cache/@kurkle-color-npm-0.3.2-98f2086013-a9e8e3e35d.zip differ diff --git a/.yarn/cache/@types-howler-npm-2.2.11-dd9e5eb3f8-0a6b6d3585.zip b/.yarn/cache/@types-howler-npm-2.2.11-dd9e5eb3f8-0a6b6d3585.zip new file mode 100644 index 0000000..e76c446 Binary files /dev/null and b/.yarn/cache/@types-howler-npm-2.2.11-dd9e5eb3f8-0a6b6d3585.zip differ diff --git a/.yarn/cache/@types-react-howler-npm-5.2.3-e13418b6ae-8c5ddded67.zip b/.yarn/cache/@types-react-howler-npm-5.2.3-e13418b6ae-8c5ddded67.zip new file mode 100644 index 0000000..d7a4b4f Binary files /dev/null and b/.yarn/cache/@types-react-howler-npm-5.2.3-e13418b6ae-8c5ddded67.zip differ diff --git a/.yarn/cache/chart.js-npm-4.4.1-bea8f3ff67-ef0cd10118.zip b/.yarn/cache/chart.js-npm-4.4.1-bea8f3ff67-ef0cd10118.zip new file mode 100644 index 0000000..9ef1ce5 Binary files /dev/null and b/.yarn/cache/chart.js-npm-4.4.1-bea8f3ff67-ef0cd10118.zip differ diff --git a/.yarn/cache/fscreen-npm-1.2.0-cbeb194fbc-1a5cb44446.zip b/.yarn/cache/fscreen-npm-1.2.0-cbeb194fbc-1a5cb44446.zip new file mode 100644 index 0000000..25c75b2 Binary files /dev/null and b/.yarn/cache/fscreen-npm-1.2.0-cbeb194fbc-1a5cb44446.zip differ diff --git a/.yarn/cache/howler-npm-2.2.4-689ccf19b1-74a295f56f.zip b/.yarn/cache/howler-npm-2.2.4-689ccf19b1-74a295f56f.zip new file mode 100644 index 0000000..6eb6a87 Binary files /dev/null and b/.yarn/cache/howler-npm-2.2.4-689ccf19b1-74a295f56f.zip differ diff --git a/.yarn/cache/react-chartjs-2-npm-5.2.0-03632f5179-437e443a26.zip b/.yarn/cache/react-chartjs-2-npm-5.2.0-03632f5179-437e443a26.zip new file mode 100644 index 0000000..3dfacbe Binary files /dev/null and b/.yarn/cache/react-chartjs-2-npm-5.2.0-03632f5179-437e443a26.zip differ diff --git a/.yarn/cache/react-full-screen-npm-1.1.1-4d2e312e1b-02c6034c6a.zip b/.yarn/cache/react-full-screen-npm-1.1.1-4d2e312e1b-02c6034c6a.zip new file mode 100644 index 0000000..ece7c90 Binary files /dev/null and b/.yarn/cache/react-full-screen-npm-1.1.1-4d2e312e1b-02c6034c6a.zip differ diff --git a/.yarn/cache/react-howler-npm-5.2.0-211ee607c8-b4b4572851.zip b/.yarn/cache/react-howler-npm-5.2.0-211ee607c8-b4b4572851.zip new file mode 100644 index 0000000..3c4ff4e Binary files /dev/null and b/.yarn/cache/react-howler-npm-5.2.0-211ee607c8-b4b4572851.zip differ diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index f4bd121..f5e55eb 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/packages/admin/package.json b/packages/admin/package.json index b7e10ce..22c315c 100644 --- a/packages/admin/package.json +++ b/packages/admin/package.json @@ -12,7 +12,9 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "chart.js": "^4.4.1", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.1" }, diff --git a/packages/admin/src/App.tsx b/packages/admin/src/App.tsx index d668bfe..b07b255 100644 --- a/packages/admin/src/App.tsx +++ b/packages/admin/src/App.tsx @@ -1,22 +1,28 @@ -import { Route, Routes } from 'react-router-dom'; +import { Link, Route, Routes } from 'react-router-dom'; import './App.css'; import Board from './components/Board/Board.tsx'; import Nav from './components/Nav.tsx'; import SystemInfo from './components/SystemInfo/SystemInfo.tsx'; +import ExceptionStatistics from './components/ExceptionStatistics/ExceptionStatistics.tsx'; function App() { return ( <> Home} /> } /> } /> + } + /> ); diff --git a/packages/admin/src/components/ExceptionStatistics/ExceptionChart/ExceptionChart.styles.tsx b/packages/admin/src/components/ExceptionStatistics/ExceptionChart/ExceptionChart.styles.tsx new file mode 100644 index 0000000..5886cc2 --- /dev/null +++ b/packages/admin/src/components/ExceptionStatistics/ExceptionChart/ExceptionChart.styles.tsx @@ -0,0 +1,7 @@ +import styled from '@emotion/styled'; + +export const Select = styled.select` + margin-top: 20px; + margin-bottom: 20px; + padding: 5px; +`; diff --git a/packages/admin/src/components/ExceptionStatistics/ExceptionChart/ExceptionChart.tsx b/packages/admin/src/components/ExceptionStatistics/ExceptionChart/ExceptionChart.tsx new file mode 100644 index 0000000..9bf3c6a --- /dev/null +++ b/packages/admin/src/components/ExceptionStatistics/ExceptionChart/ExceptionChart.tsx @@ -0,0 +1,126 @@ +import { Exception, ExceptionConditions } from '../exception.interface.ts'; +import { Bar } from 'react-chartjs-2'; +import { CategoryScale } from 'chart.js'; +import Chart from 'chart.js/auto'; +import React, { useState } from 'react'; +import { Select } from './ExceptionChart.styles.tsx'; +Chart.register(CategoryScale); + +interface PropsType { + exceptionData: Exception[]; + condition: ExceptionConditions; +} + +export default function ExceptionChart({ + exceptionData, + condition, +}: PropsType) { + const filteredExceptionData = filter(exceptionData, condition); + const uniqueDate = getUniqueDate(filteredExceptionData); + + const [option, setOption] = useState('path'); + + const handleChangeOption = (event: React.ChangeEvent) => { + setOption(event.target.value); + }; + + return ( +
+ + +
+ ); +} + +function filter( + exceptionData: Exception[], + condition: ExceptionConditions, +): Exception[] { + const filterdData = exceptionData.filter((exception: Exception) => { + const { startDate, endDate } = condition; + const timestamp = new Date(exception.timestamp); + if (!startDate && !endDate) return true; + if (startDate && !endDate) return timestamp >= startDate; + if (!startDate && endDate) return timestamp <= endDate; + if (startDate && endDate) + return timestamp >= startDate && timestamp <= endDate; + }); + return filterdData; +} + +function getUniqueDate(exceptionData: Exception[]) { + const uniqueDate = new Set( + exceptionData.map((exception: Exception) => { + return exception.timestamp.toLocaleString().slice(0, 10); + }), + ); + return [...uniqueDate]; +} + +function getDatasets( + exceptionData: Exception[], + uniqueDate: string[], + option: string, +) { + if (option === 'path') { + const uniquePathList = new Set( + exceptionData.map((exception: Exception) => exception.path), + ); + const datasets = [...uniquePathList].map((path: string) => { + const filteredData = uniqueDate.map((date: string) => { + return exceptionData.filter( + (exception: Exception) => + exception.path === path && + exception.timestamp.toLocaleString().slice(0, 10) === date, + ); + }); + const randomColor = getRandomColor(); + return { + label: path, + data: filteredData.map((data: Exception[]) => data.length), + backgroundColor: randomColor, + borderColor: randomColor, + borderWidth: 1, + }; + }); + return datasets; + } + + const uniqueErrorList = new Set( + exceptionData.map((exception: Exception) => exception.error), + ); + const datasets = [...uniqueErrorList].map((error: string) => { + const filteredData = uniqueDate.map((date: string) => { + return exceptionData.filter( + (exception: Exception) => + exception.error === error && + exception.timestamp.toLocaleString().slice(0, 10) === date, + ); + }); + const randomColor = getRandomColor(); + return { + label: error, + data: filteredData.map((data: Exception[]) => data.length), + backgroundColor: randomColor, + borderColor: randomColor, + borderWidth: 1, + }; + }); + return datasets; +} + +function getRandomColor() { + const r = Math.floor(Math.random() * 256); + const g = Math.floor(Math.random() * 256); + const b = Math.floor(Math.random() * 256); + + return `rgb(${r}, ${g}, ${b})`; +} diff --git a/packages/admin/src/components/ExceptionStatistics/ExceptionStatistics.tsx b/packages/admin/src/components/ExceptionStatistics/ExceptionStatistics.tsx new file mode 100644 index 0000000..7870211 --- /dev/null +++ b/packages/admin/src/components/ExceptionStatistics/ExceptionStatistics.tsx @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react'; +import Filter from './Filter/Filter.tsx'; +import ExceptionChart from './ExceptionChart/ExceptionChart.tsx'; +import { Exception } from './exception.interface.ts'; + +const baseUrl = import.meta.env.VITE_API_BASE_URL; + +export default function ExceptionStatistics() { + const [condition, setCondition] = useState({}); + const [exceptionData, setExceptionData] = useState([]); + + const getAllExceptions = async () => { + const response = await fetch(baseUrl + '/admin/exception'); + const exceptions = await response.json(); + exceptions.map((exception: Exception) => { + if (exception.path.includes('?')) { + exception.path = exception.path.slice(0, exception.path.indexOf('?')); + } + }); + setExceptionData(exceptions); + }; + + useEffect(() => { + getAllExceptions(); + }, []); + + return ( +
+ + +
+ ); +} diff --git a/packages/admin/src/components/ExceptionStatistics/Filter/Filter.styles.tsx b/packages/admin/src/components/ExceptionStatistics/Filter/Filter.styles.tsx new file mode 100644 index 0000000..2c76282 --- /dev/null +++ b/packages/admin/src/components/ExceptionStatistics/Filter/Filter.styles.tsx @@ -0,0 +1,23 @@ +import styled from '@emotion/styled'; + +export const FilterContainer = styled.div` + padding: 1rem; + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; +`; + +export const DateInputContainer = styled.div` + background-color: ${(props) => props.color}; + width: 150px; +`; + +export const DateInputTitle = styled.p` + font-size: 1.5rem; + border-bottom: 1px solid white; +`; + +export const DateInput = styled.input` + width: 128.75px; +`; diff --git a/packages/admin/src/components/ExceptionStatistics/Filter/Filter.tsx b/packages/admin/src/components/ExceptionStatistics/Filter/Filter.tsx new file mode 100644 index 0000000..efe742b --- /dev/null +++ b/packages/admin/src/components/ExceptionStatistics/Filter/Filter.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +import Button from '../../shared/Button.tsx'; +import { ExceptionConditions } from '../exception.interface.ts'; +import { + DateInput, + DateInputContainer, + DateInputTitle, + FilterContainer, +} from './Filter.styles.tsx'; + +interface PropsType { + setCondition: React.Dispatch>; +} + +export default function Filter({ setCondition }: PropsType) { + const [defaultStartDate, defaultEndDate] = getDates(); + + const [startDateCondition, setStartDateCondition] = + useState(defaultStartDate); + const [endDateCondition, setEndDateCondition] = useState(defaultEndDate); + + const selectStartDate = (event: React.ChangeEvent) => { + setStartDateCondition(event.target.value); + }; + const selectEndDate = (event: React.ChangeEvent) => { + setEndDateCondition(event.target.value); + }; + + const changeCondition = () => { + setCondition({ + startDate: new Date(startDateCondition), + endDate: new Date( + new Date(endDateCondition).getTime() + 60 * 60 * 24 * 1000, + ), + }); + }; + + return ( + + + 시작 날짜 + + + + 종료 날짜 + + + + + ); +} + +function getDates() { + const currentDate = new Date(); + const year = currentDate.getFullYear(); + const month = String(currentDate.getMonth() + 1).padStart(2, '0'); + const day = String(currentDate.getDate()).padStart(2, '0'); + const defaultStartDate = `${year}-${month}-01`; + const defaultEndDate = `${year}-${month}-${day}`; + return [defaultStartDate, defaultEndDate]; +} diff --git a/packages/admin/src/components/ExceptionStatistics/exception.interface.ts b/packages/admin/src/components/ExceptionStatistics/exception.interface.ts new file mode 100644 index 0000000..96a337e --- /dev/null +++ b/packages/admin/src/components/ExceptionStatistics/exception.interface.ts @@ -0,0 +1,13 @@ +export interface Exception { + _id: string; + _v: number; + error: string; + message: string; + path: string; + timestamp: Date; +} + +export interface ExceptionConditions { + startDate?: Date; + endDate?: Date; +} diff --git a/packages/admin/src/components/shared/Table.tsx b/packages/admin/src/components/shared/Table.tsx index 11f0bb6..a6adb8e 100644 --- a/packages/admin/src/components/shared/Table.tsx +++ b/packages/admin/src/components/shared/Table.tsx @@ -18,7 +18,7 @@ const CustomTable = styled.table` export const TH = styled.th` border: 1px solid #ddd; padding: 10px; - background-color: #3e3e3e; + background-color: #d8d8d8; `; export const TD = styled.td` diff --git a/packages/client/package.json b/packages/client/package.json index 35cea5f..e9d5c63 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -20,6 +20,7 @@ "react": "^18.2.0", "react-color": "^2.19.3", "react-dom": "^18.2.0", + "react-full-screen": "^1.1.1", "react-markdown": "^9.0.1", "react-router-dom": "^6.18.0", "remark-gfm": "^4.0.0", @@ -33,6 +34,7 @@ "@types/react": "^18.2.15", "@types/react-color": "^3.0.10", "@types/react-dom": "^18.2.7", + "@types/react-howler": "^5.2.3", "@types/three": "^0.158.2", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", @@ -43,6 +45,7 @@ "eslint-plugin-react-refresh": "^0.4.3", "js-cookie": "^3.0.5", "leva": "^0.9.35", + "react-howler": "^5.2.0", "three-stdlib": "^2.28.5", "typescript": "^5.0.2", "vite": "^4.4.5", diff --git a/packages/client/src/assets/bgm.mp3 b/packages/client/src/assets/bgm.mp3 new file mode 100644 index 0000000..8eaf0af Binary files /dev/null and b/packages/client/src/assets/bgm.mp3 differ diff --git a/packages/client/src/entities/posts/ui/Post.tsx b/packages/client/src/entities/posts/ui/Post.tsx index 647590d..928449d 100644 --- a/packages/client/src/entities/posts/ui/Post.tsx +++ b/packages/client/src/entities/posts/ui/Post.tsx @@ -64,7 +64,7 @@ export default function Post({ data, postId, title }: PropsType) { brightness={data.brightness} shape={data.shape} > - {view === 'DETAIL' && isHovered && ( + {view !== 'POST' && isHovered && ( diff --git a/packages/client/src/features/audio/Audio.tsx b/packages/client/src/features/audio/Audio.tsx new file mode 100644 index 0000000..a22653e --- /dev/null +++ b/packages/client/src/features/audio/Audio.tsx @@ -0,0 +1,17 @@ +import bgm from 'assets/bgm.mp3'; +import ReactHowler from 'react-howler'; +import { usePlayingStore } from 'shared/store/useAudioStore'; + +export default function Audio() { + const { playing } = usePlayingStore(); + + return ( + + ); +} diff --git a/packages/client/src/features/controls/Controls.tsx b/packages/client/src/features/controls/Controls.tsx index c4f0ef2..5287e0d 100644 --- a/packages/client/src/features/controls/Controls.tsx +++ b/packages/client/src/features/controls/Controls.tsx @@ -37,7 +37,7 @@ export default function Controls() { useFrame((state, delta) => { const targetPosition = new THREE.Vector3(0, 0, 0); - const LENGTH_LIMIT = (cameraToCurrentView * delta) / 2; + const LENGTH_LIMIT = ((cameraToCurrentView + 5000) * delta) / 2; if (targetView) targetView.getWorldPosition(targetPosition); if (view === 'POST') { diff --git a/packages/client/src/features/star/Star.tsx b/packages/client/src/features/star/Star.tsx index 7a6c8a2..9adea16 100644 --- a/packages/client/src/features/star/Star.tsx +++ b/packages/client/src/features/star/Star.tsx @@ -36,7 +36,7 @@ const Star = forwardRef((props, ref) => { const cameraDistance = innerRef.current.position.distanceTo( state.camera.position, ); - const scale = cameraDistance / DISTANCE_LIMIT; + const scale = Math.log((cameraDistance / DISTANCE_LIMIT) * Math.E); if (cameraDistance > DISTANCE_LIMIT) { innerRef.current!.scale.x = scale; @@ -57,18 +57,16 @@ const Star = forwardRef((props, ref) => { onPointerOut={onPointerOut} > - {/* */} - + {/* + /> */} {children} ); diff --git a/packages/client/src/features/writingModal/ui/WritingModal.tsx b/packages/client/src/features/writingModal/ui/WritingModal.tsx index 746877f..329ea7c 100644 --- a/packages/client/src/features/writingModal/ui/WritingModal.tsx +++ b/packages/client/src/features/writingModal/ui/WritingModal.tsx @@ -6,16 +6,25 @@ import Images from './Images'; import { useNavigate } from 'react-router-dom'; import { useViewStore, usePostStore } from 'shared/store'; import InputBar from 'shared/ui/inputBar/InputBar'; +import styled from '@emotion/styled'; +import { css } from '@emotion/react'; +import { Caption } from 'shared/ui/styles'; + +type TextStateTypes = 'DEFAULT' | 'INVALID'; export default function WritingModal() { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [files, setFiles] = useState(null); + const [titleState, setTitleState] = useState('DEFAULT'); + const [contentState, setContentState] = useState('DEFAULT'); const navigate = useNavigate(); const { setView } = useViewStore(); const { setStoreTitle, setStoreContent, setStoreFiles } = usePostStore(); const handleSendPost = async () => { + if (title === '') return setTitleState('INVALID'); + if (content === '') return setContentState('INVALID'); setStoreTitle(title); setStoreContent(content); setStoreFiles(files); @@ -42,15 +51,76 @@ export default function WritingModal() { navigate('/home'); }} > - setTitle(e.target.value)} + + { + setTitleState('DEFAULT'); + setTitle(e.target.value); + }} + state={titleState} + /> + {titleState === 'INVALID' && 제목을 입력해주세요.} + + { + setContentState('DEFAULT'); + setContent(content); + }} + height="40vh" + state={contentState} /> -