diff --git a/back-end/controllers/chartController.js b/back-end/controllers/chartController.js new file mode 100644 index 00000000..c85c1840 --- /dev/null +++ b/back-end/controllers/chartController.js @@ -0,0 +1,160 @@ +const asyncHandler = require("express-async-handler"); +const UnitM = require("../models/unitModel"); +const Chart = require("../models/chartModel"); + +//@description Get organizational chart as array list +//@route GET /api/chart +//@access Protected +const getChart = asyncHandler(async (req, res) => { + const currentUser = req.user; + const currentUnit = await UnitM.findOne({ _id: currentUser.Unit }); + + if (!currentUnit) { + res.status(400); + throw new Error('사용자의 부대가 설정되지 않았습니다.'); + } + + const unitOrganization = await Chart.find({ Unit: { $eq: currentUnit.Unitname } }); + if (unitOrganization.length === 0) { + await Chart.create({ + Name: currentUser.Name, + Rank: currentUser.Rank, + Unit: currentUnit.Unitname + }) + } + res.send(unitOrganization); +}); + +//@description Add organizational chart +//@route POST /api/chart/add +//@access Protected +const addChart = asyncHandler(async (req, res) => { + const { + Name, + Rank, + Unit, + Position, + DoDID, + Number, + MilNumber, + Email, + Parent + } = req.body; + + if (!Name || !Rank || !Unit) { + res.status(400); + throw new Error("모든 정보를 입력하세요."); + } + + const newChart = await Chart.create({ + Name, + Rank, + Unit, + Position, + DoDID, + Number, + MilNumber, + Email, + Parent, + }); + + if (newChart) { + res.status(201).json({ + _id: newChart._id, + Name, + Rank, + Unit, + Position, + DoDID, + Number, + MilNumber, + Email, + Parent: Parent ?? null, + }) + } + else { + res.send(400); + throw new Error('조직도를 추가/수정할 수 없습니다.') + } +}); + +//@description Edit organizational chart +//@route POST /api/chart/edit +//@access Protected +const editChart = asyncHandler(async (req, res) => { + const { + _id, + Name, + Rank, + Unit, + Position, + DoDID, + Number, + MilNumber, + Email, + Parent + } = req.body; + + if (!_id || !Name || !Rank || !Unit) { + res.status(400); + throw new Error("모든 정보를 입력하세요."); + } + + const updatedChart = await Chart.findByIdAndUpdate(_id, { + Name, + Rank, + Position, + DoDID, + Number, + MilNumber, + Email, + Parent, + Unit + }); + + if (updatedChart) { + res.status(200).json({ + Name, + Rank, + Position, + DoDID, + Number, + MilNumber, + Email, + Parent, + Unit + }) + } + else { + res.send(400); + throw new Error('조직도를 추가/수정할 수 없습니다.') + } +}); + +//@description Delete organizational chart +//@route POST /api/chart/delete +//@access Protected +const deleteChart = asyncHandler(async (req, res) => { + const { _id } = req.body; + + if (!_id) { + res.status(400); + throw new Error("모든 정보를 입력하세요."); + } + + const deleteChart = await Chart.findByIdAndDelete(_id); + if (deleteChart) { + res.send(200); + } + else { + res.send(400); + throw new Error('조직도를 추가/수정할 수 없습니다.') + } +}); + +module.exports = { + getChart, + addChart, + editChart, + deleteChart +}; \ No newline at end of file diff --git a/back-end/controllers/reportControllers.js b/back-end/controllers/reportControllers.js index b1786cc3..dc24e2b8 100644 --- a/back-end/controllers/reportControllers.js +++ b/back-end/controllers/reportControllers.js @@ -4,8 +4,6 @@ const Reportsys = require("../models/reportModel"); const UnitM = require("../models/unitModel"); const UserM = require("../models/userModel"); const getScore = require('../ai/classifier.js') -var mongoose = require('mongoose'); -const jwt = require("jsonwebtoken"); //@description Get all report cards //@route GET /api/report @@ -26,7 +24,6 @@ const addReportCard = asyncHandler(async (req, res) => { Invited, Content, Title, - UserToken } = req.body; if (!Type || !ReportingSystem || !Invited || !Content || !Title) { @@ -34,12 +31,9 @@ const addReportCard = asyncHandler(async (req, res) => { throw new Error("모든 정보를 입력하세요."); } - //decodes token id - const decoded = jwt.verify(UserToken, process.env.JWT_SECRET); - const currentUser = await UserM.findById(decoded.id).select("-password"); - - let currentUnit = currentUser.Unit - let Severity = await getScore(Content) + const currentUser = req.user; + let currentUnit = currentUser.Unit; + let Severity = await getScore(Content); const report = await Report.create({ User: currentUser, Type, diff --git a/back-end/models/chartModel.js b/back-end/models/chartModel.js new file mode 100644 index 00000000..3ff26d4e --- /dev/null +++ b/back-end/models/chartModel.js @@ -0,0 +1,41 @@ +const mongoose = require("mongoose"); + +const chartModel = mongoose.Schema({ + Name: { + type: String, + required: true + }, + Rank: { + type: String, + required: true + }, + Unit: { + type: String, + required: true + }, + Position: { + type: String + }, + DoDID: { + type: String + }, + Number: { + type: String + }, + MilNumber: { + type: String + }, + Email: { + type: String + }, + Parent: { + type: String, + default: null + } +}, { + timestamps: true +}); + +const Chart = mongoose.model("Chart", chartModel); + +module.exports = Chart; diff --git a/back-end/routes/chartRoutes.js b/back-end/routes/chartRoutes.js new file mode 100644 index 00000000..372a39b0 --- /dev/null +++ b/back-end/routes/chartRoutes.js @@ -0,0 +1,20 @@ +const express = require("express"); +const { + getChart, + addChart, + editChart, + deleteChart +} = require('../controllers/chartController'); +const { + protect, + onlyAdmin +} = require("../middleware/authMiddleware"); + +const router = express.Router(); + +router.route("/").get(protect, getChart); +router.route("/add").post(onlyAdmin, addChart); +router.route("/edit").post(onlyAdmin, editChart); +router.route("/delete").post(onlyAdmin, deleteChart); + +module.exports = router; diff --git a/back-end/server.js b/back-end/server.js index 4629c026..f34c8c75 100644 --- a/back-end/server.js +++ b/back-end/server.js @@ -8,6 +8,7 @@ const messageRoutes = require("./routes/messageRoutes"); const unitRoutes = require("./routes/unitRoutes"); const reportRoutes = require("./routes/reportRoutes"); const commentRoutes = require("./routes/commentRoutes"); +const chartRoutes = require("./routes/chartRoutes"); const reportsysRoutes = require("./routes/reportsysRoutes"); const { notFound, @@ -33,6 +34,7 @@ app.use("/api/unit", unitRoutes); app.use("/api/report", reportRoutes); app.use("/api/comment", commentRoutes); app.use("/api/reportsys", reportsysRoutes); +app.use("/api/chart", chartRoutes); /*const __dirname1 = path.resolve(); diff --git a/front-end/web-next/componenets/MemoForm.js b/front-end/web-next/componenets/MemoForm.js index eb37a6db..c68a2ffd 100644 --- a/front-end/web-next/componenets/MemoForm.js +++ b/front-end/web-next/componenets/MemoForm.js @@ -183,8 +183,7 @@ function MemoForm(props) { Type: memoType, ReportingSystem: reportOrgList.map((org) => (org.title)), Invited: addUserList, - Content: memoContent, - UserToken: getCookie('usercookie') + Content: memoContent } await fetch(process.env.NEXT_PUBLIC_BACKEND_ROOT + 'api/report/', { diff --git a/front-end/web-next/componenets/OrganizationCard.js b/front-end/web-next/componenets/OrganizationCard.js index 2ebcc302..f00e93d1 100644 --- a/front-end/web-next/componenets/OrganizationCard.js +++ b/front-end/web-next/componenets/OrganizationCard.js @@ -19,6 +19,7 @@ function OrganizationCard(props) { return ( <> + +
- {props.data.name} - {props.data.rank} + {props.data.Name} + {props.data.Rank}
-
{props.data.DoDID}
- - + + - - + + - - + +
setOpenCreateForm(false)} + data={{ Unit: props.data.Unit }} onSubmit={props.onCreate} nodeList={props.nodeList} /> setOpenUpdateForm(false)} - onSubmit={props.onUpdate} data={props.data} + onSubmit={props.onUpdate} nodeList={props.nodeList} /> diff --git a/front-end/web-next/componenets/OrganizationForm.js b/front-end/web-next/componenets/OrganizationForm.js index bfc328a7..aee964bb 100644 --- a/front-end/web-next/componenets/OrganizationForm.js +++ b/front-end/web-next/componenets/OrganizationForm.js @@ -1,12 +1,21 @@ import { useState, useCallback, useEffect } from 'react'; -import { Modal, Image, Row, Col, Select } from 'antd'; +import { Modal, Image, Row, Col, Input, Select } from 'antd'; import Styles from '../styles/OrganizationForm.module.css'; +function InfoElement(props) { + return ( +
+
{props.label}
+
{props.content}
+
+ ) +} + function InputElement(props) { return (
-
{props.label}
- +
{props.label}
+
) } @@ -14,7 +23,7 @@ function InputElement(props) { function ParentSelectElement(props) { return (
-
{props.label}
+
{props.label}
serializedEdit('name', event.target.value)} /> + + serializedEdit('Name', event.target.value)} + /> -
- serializedEdit('DoDID', event.target.value)} /> + + - serializedEdit('unit', event.target.value)} /> - - - serializedEdit('position', event.target.value)} /> + serializedEdit('DoDID', event.target.value)} /> - serializedEdit('roles', event.target.value)} /> + serializedEdit('Position', event.target.value)} /> - serializedEdit('email', event.target.value)} /> + serializedEdit('Email', event.target.value)} /> - serializedEdit('number', event.target.value)} /> + serializedEdit('Number', event.target.value)} /> - serializedEdit('milNumber', event.target.value)} /> + serializedEdit('MilNumber', event.target.value)} /> - + serializedEdit('parent', key)} - selfKey={formData.key} - nodeList={props.nodeList} + value={formData.Parent} + onChange={({ key }) => serializedEdit('Parent', key === 'null' ? null : key)} + selfKey={formData._id} + nodeList={[{ 'key': null, 'value': '없음' }, ...props.nodeList]} /> diff --git a/front-end/web-next/componenets/Organogram.js b/front-end/web-next/componenets/Organogram.js index 43fca12a..57777494 100644 --- a/front-end/web-next/componenets/Organogram.js +++ b/front-end/web-next/componenets/Organogram.js @@ -1,6 +1,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Tree, TreeNode } from 'react-organizational-chart'; import { Button, Image, Row, Col } from 'antd'; +import { getCookie } from 'cookies-next'; import OrganizationCard from './OrganizationCard'; import Styles from '../styles/Organogram.module.css'; @@ -8,15 +9,18 @@ function renderNode(node, chooseNode) { if (!node) return; - if (!node.parent) + if (!node.Parent) return ( chooseNode(node)} /> } @@ -27,12 +31,12 @@ function renderNode(node, chooseNode) { return ( chooseNode(node)} /> } @@ -50,7 +54,7 @@ function TreeNodeElement(props) { >
@@ -83,8 +87,21 @@ function Organogram(props) { const [orgDataTree, setOrgDataTree] = useState([]); useEffect(() => { - setOrgData(props.renderData); - }, [props.renderData]); + fetch(process.env.NEXT_PUBLIC_BACKEND_ROOT + 'api/chart', { + 'method': 'GET', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getCookie('usercookie')}` + }, + }) + .then(response => response.json()) + .then(data => { + const objectData = data.reduce((preData, node) => ( + { ...preData, [node._id]: node } + ), {}); + setOrgData(objectData) + }); + }, []); useEffect(() => { makeTree(orgData); @@ -99,49 +116,90 @@ function Organogram(props) { setCardOpened(true); }, [setSelectedOrgInfo, setCardOpened]); - const createNode = useCallback((node) => { - // node key generate - const randomKey = Math.random().toString(36).substring(2, 10); - node['key'] = randomKey; - - setOrgData(treeNode => ({ ...treeNode, [randomKey]: node })); + const createNode = useCallback(async (node) => { + await fetch(process.env.NEXT_PUBLIC_BACKEND_ROOT + 'api/chart/add', { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getCookie('usercookie')}` + }, + 'body': JSON.stringify(node) + }) + .then(response => { + if (response.status === 200 || response.status === 201) + return response.json() + }) + .then(data => { + if (data) + setOrgData(treeNode => ({ ...treeNode, [data._id]: data })) + }) }, [setOrgData]); - const updateNode = useCallback((node) => { - setOrgData(treeNode => ({ ...treeNode, [node.key]: node })); + const updateNode = useCallback(async (node) => { + await fetch(process.env.NEXT_PUBLIC_BACKEND_ROOT + 'api/chart/edit', { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getCookie('usercookie')}` + }, + 'body': JSON.stringify(node) + }) + .then((response) => { + if (response.status === 200 || response.status === 201) + setOrgData(treeNode => ({ ...treeNode, [node._id]: node })) + }) }, [setOrgData]); - const deleteNode = useCallback((node) => { - setOrgData(treeNode => { - const nodeCopy = { ...treeNode }; - - // Redirection for children of removed node - const nodeChildren = Object.values(nodeCopy).filter((data) => data.parent == node.key); - for (let child of nodeChildren) - child.parent = node.parent; + const deleteNode = useCallback(async (node) => { + const nodeCopy = { ...orgData }; + + // Redirection for children of removed node + const nodeChildren = Object.values(nodeCopy).filter((data) => data.Parent == node._id); + for (let child of nodeChildren) { + child.Parent = node.Parent; + await fetch(process.env.NEXT_PUBLIC_BACKEND_ROOT + 'api/chart/edit', { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getCookie('usercookie')}` + }, + 'body': JSON.stringify(child) + }); + } - delete nodeCopy[node.key] - return nodeCopy; - }); - }, [setOrgData]); + await fetch(process.env.NEXT_PUBLIC_BACKEND_ROOT + 'api/chart/delete', { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getCookie('usercookie')}` + }, + 'body': JSON.stringify(node) + }) + .then((response) => { + if (response.status === 200 || response.status === 201) { + delete nodeCopy[node._id]; + setOrgData(nodeCopy); + } + }) + }, [orgData, setOrgData]); const makeTree = useCallback((data) => { // Deep Copy for object const dataSet = {}; - for (let key in data) - dataSet[key] = Object.assign({}, data[key]); + for (let id in data) + dataSet[id] = { ...data[id] }; const dataTree = []; - for (let key in dataSet) { - if (!dataSet[key].parent) { - dataTree.push(dataSet[key]); + for (let id in dataSet) { + if (!dataSet[id].Parent) { + dataTree.push(dataSet[id]); } else { - if (dataSet[dataSet[key].parent].children) { - dataSet[dataSet[key].parent].children.push(dataSet[key]); + if (dataSet[dataSet[id].Parent].children) { + dataSet[dataSet[id].Parent].children.push(dataSet[id]); } else { - dataSet[dataSet[key].parent].children = [dataSet[key]]; + dataSet[dataSet[id].Parent].children = [dataSet[id]]; } } } @@ -153,7 +211,7 @@ function Organogram(props) { <> {orgDataTree.map((dataNode) => ( - + {renderNode(dataNode, chooseOrgInfo)} ))} @@ -166,7 +224,7 @@ function Organogram(props) { onCreate={createNode} onUpdate={updateNode} onRemove={deleteNode} - nodeList={props.renderData && Object.values(props.renderData).map((node) => ({ 'key': node.key, 'value': node.rank + ' ' + node.name }))} + nodeList={orgData && Object.values(orgData).map((node) => ({ 'key': node._id, 'value': node.Rank + ' ' + node.Name }))} /> ) diff --git a/front-end/web-next/styles/OrganizationCard.module.css b/front-end/web-next/styles/OrganizationCard.module.css index aaba0e3f..b3db5680 100644 --- a/front-end/web-next/styles/OrganizationCard.module.css +++ b/front-end/web-next/styles/OrganizationCard.module.css @@ -1,26 +1,21 @@ .profileImage { - width: 100px; - height: 100px; + width: 150px; + height: 150px; border: 1px solid #ddd; border-radius: 100%; } .userName { - font-size: 14pt; + font-size: 18pt; font-weight: bold; } .milRank { - font-size: 12pt; + font-size: 16pt; font-weight: bold; margin-left: 5pt; } -.userDodid { - font-size: 12pt; - color: #444; -} - .infoLabel { font-size: 14pt; font-weight: bold; @@ -32,9 +27,9 @@ .elementRow { margin: 7px 0px; - padding: 0px 15px; + padding: 0px 25px; } .userProfile { - margin-left: 15px; + margin-bottom: 15px; } \ No newline at end of file diff --git a/front-end/web-next/styles/OrganizationForm.module.css b/front-end/web-next/styles/OrganizationForm.module.css index e82d584b..85c9c29b 100644 --- a/front-end/web-next/styles/OrganizationForm.module.css +++ b/front-end/web-next/styles/OrganizationForm.module.css @@ -1,14 +1,38 @@ .profileImage { - width: 100px; - height: 100px; + width: 150px; + height: 150px; border: 1px solid #ddd; border-radius: 100%; } +.userProfile { + margin-bottom: 15px; +} + .formSelect { width: 100px; } .parentSelect { width: 300px; +} + +.infoLabel { + font-size: 14pt; + font-weight: bold; +} + +.infoContent { + font-size: 12pt; +} + +.formInput { + width: 180px; + height: 30px; + border: 1px solid #d9d9d9; +} + +.elementRow { + margin: 7px 0px; + padding: 0px 25px; } \ No newline at end of file diff --git a/front-end/web-next/styles/Organogram.module.css b/front-end/web-next/styles/Organogram.module.css index 7c5e463c..0bf27b2b 100644 --- a/front-end/web-next/styles/Organogram.module.css +++ b/front-end/web-next/styles/Organogram.module.css @@ -2,6 +2,8 @@ width: auto; height: auto; padding: 0; + margin-left: 15px; + margin-right: 15px; border: transparent; border-radius: 30pt; background-color: transparent @@ -11,13 +13,14 @@ width: 100px; height: 100px; border: 1px solid #ddd; - border-radius: 100% + border-radius: 50px } .cardContent { - padding: 10px; - border: 1px solid #777; - border-radius: 30pt; + padding-right: 50px; + border: 0; + border-radius: 50pt; + box-shadow: inset 0 -3em 3em rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 128, 128, 0.5), 0.3em 0.3em 1em rgba(0, 0, 0, 0.3); } .userName { @@ -32,5 +35,7 @@ } .userPosition { + color: #555; font-size: 13pt; + text-align: left; } \ No newline at end of file diff --git a/front-end/web-next/styles/globals.css b/front-end/web-next/styles/globals.css index bbc3936b..002502c6 100644 --- a/front-end/web-next/styles/globals.css +++ b/front-end/web-next/styles/globals.css @@ -116,4 +116,10 @@ a { .ant-spin-dot-item { background-color: #008080; +} + +.organizationCard > .ant-modal-content, +.organizationForm > .ant-modal-content { + border-radius: 30px; + padding: 5px; } \ No newline at end of file