일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 밸류즈
- 스프링시큐리티
- 회원가입로직
- RCPS
- 로그인
- MRC
- Ajax
- 인증처리
- 밸류즈 홈페이지
- Styled Components
- 공통메서드
- Token
- react
- 이미지 업로드
- Typesciprt
- 관리자페이지
- 빌드 및 배포
- Update
- 파생상품평가
- register
- 로그인 로직
- 캘린더 라이브러리
- jsonwebtoken
- 달력 라이브러리
- stock option
- 배포
- userManagement
- 마이페이지
- ui탬플릿
- mypage
- Today
- Total
I T H
[MySchedule project] 20. 관리자 - 로그관리 페이지 구현 본문
관리자 페이지의 로그에 관련된 페이지를 작성했다.
로그 관리에 대한 주제는 여러가지가 있지만, 그중에서 로그인이력을 볼수 있는 부분을 구현했다.
관리자가 로그인한 이력을 볼수있고, 조건을 아이디와 시작날짜 또는 종료날짜로 주어서 원하는 데이터를 출력할수 있게 하였다.
사용 라이브러리는 react-datepicker와 moment.js, mui grid를 사용했다.
해당 라이브러리들은 그동안의 챕터에서 설치하는 부분을 작성했으므로, 이번 챕터에서는 그부분에 대한 설명은 생략하도록 하겠다.
Frontend
[ src / pages / LogManagement / index.tsx ]
- 데이터 조회 조건(관리자로부터 입력을 받게 되는 input의 값)에 대한 부분을 작성해준다. 아이디, 시작날짜, 종료날짜가 있는데 이때 라이브러리 react-datepicker를 사용한다.
react-datepicker에 대한 자세한 설명은 아래 링크를 참고한다.
[ React ] react-datepicker 사용하기
- react-datepicker는 버튼 클릭시 아래와 같이 달력을 화면에 보이게 하고, 날짜를 선택하면 적용시킬 수 있도록 도와주는 라이브러리다. - 달력을 쉽게 커스텀해서 사용할 수 있는 장점이 있다. [ rea
devth.tistory.com
- 아이디와 시작날짜, 종료날짜에 대한 데이터를 백앤드로 보내주는 함수를 작성한다.
- db에 있는 데이터를 조회하는것이므로, 보내주는 데이터 형식도 db에 있는 데이터형식과 일치해야된다. 이때 moment라이브러리에있는 format 기능을 사용해서 일치시켜 주는데, 이부분은 백앤드에서 작성했다.
import React, { ChangeEvent, useEffect, useState } from "react";
import { DataGrid, GridColDef, GridRowsProp } from "@mui/x-data-grid";
import axiosInstance from "../../utils/axios";
import { userData, userManagementInput } from "../../types/logManagement";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import "./LogManagement.css";
import moment from "moment";
const LogManagemnet = () => {
const [textUserData, setTextUserdata] = useState<userManagementInput>({
textUserId: "",
});
const [selectedStartDate, setSelectedStartDate] = useState<Date | null>(
moment().subtract(1, "days").toDate()
);
const [selectedEndDate, setSelectedEndDate] = useState<Date | null>(
new Date()
);
const [response, setResponse] = useState<userData[]>([]); //백앤드에서 조회해온 데이터
//화면 랜더링시 데이터조회 함수 호출
useEffect(() => {
handleSelectUserData();
}, []);
//input onChange 함수
const handleChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const { name, value } = e.target;
setTextUserdata((prevState) => ({
...prevState,
[name]: value,
}));
};
//데이터 조회
const handleSelectUserData = async () => {
const body = {
...textUserData,
selectedStartDate,
selectedEndDate,
};
try {
const response = await axiosInstance.post(`/logManagement`, body);
setResponse(response.data);
} catch (error: any) {
console.log(error);
}
};
// 데이터 맵핑 start
// data 배열에 데이터 갯수만큼 push 해야 함
let rows: GridRowsProp = [];
rows = response.map((data, index) => {
let loginType = "";
//console.log(data);
if (data.loginType === 0) {
loginType = "일반로그인";
} else {
loginType = "소셜로그인";
}
return {
id: index,
userId: data.userId,
loginDatetime: data.loginDatetime,
loginType: loginType,
loginYn: data.loginYn,
};
});
//데이터 조회 end
const columns: GridColDef[] = [
{ field: "userId", headerName: "아이디", width: 150 },
{ field: "loginDatetime", headerName: "로그인시간", width: 200 },
{ field: "loginType", headerName: "로그인타입", width: 150 },
{ field: "loginYn", headerName: "로그인성공유무", width: 150 },
];
// 데이터 맵핑 end
return (
<section>
<div className="m-auto p-4 my-2 w-full">
<div className="grid md:grid-cols-4 gap-4 my-4">
<div className="">
<p className="mr-2 mb-2 pl-1">아이디</p>
<input
className="w-full placeholder:text-sm text-md rounded-md border-[1px] border-gray-200 border-t-[3px] h-[45px] p-3 hover:border-t-[3px] hover:border-t-gray-400 focus:focus:outline-none"
type="text"
id="textUserId"
name="textUserId"
onChange={handleChangeInput}
value={textUserData.textUserId}
/>{" "}
</div>
<div className="customDatePickerWidth">
<p className="mr-2 mb-2 pl-1">시작날짜</p>
<DatePicker
wrapperClassName="w-full"
className="text-md rounded-md border-[1px] border-gray-200 border-t-[3px] h-[45px] p-3 hover:border-t-[3px] hover:border-t-gray-400 focus:focus:outline-none"
dateFormat="yyyy.MM.dd" // 날짜 형태
shouldCloseOnSelect // 자동 닫힘 기능
minDate={new Date("2023-01-01")} // minDate 이전 날짜 선택 불가
//maxDate={new Date()} // maxDate 이후 날짜 선택 불가
selected={selectedStartDate}
onChange={(date) => setSelectedStartDate(date)}
/>
</div>
<div className="customDatePickerWidth">
<p className="mr-2 mb-2 pl-1">종료날짜</p>
<DatePicker
wrapperClassName="w-full"
className="w-[200px] text-md rounded-md border-[1px] border-gray-200 border-t-[3px] h-[45px] p-3 hover:border-t-[3px] hover:border-t-gray-400 focus:focus:outline-none"
dateFormat="yyyy.MM.dd"
shouldCloseOnSelect
minDate={new Date("2023-01-01")}
selected={selectedEndDate}
onChange={(date) => setSelectedEndDate(date)}
/>
</div>
<div className="flex justify-end">
<button
onClick={handleSelectUserData}
className="w-full sm:mt-8 bg-blue-500 h-[45px] text-white sm:font-bold font-semibold sm:text-sm text-[12px] rounded-md hover:bg-blue-600"
>
조회
</button>
</div>
</div>
</div>
{/* grid 사용 */}
<div>
<DataGrid
rows={rows}
columns={columns}
//onRowClick={getSelectedRows}
checkboxSelection
//onRowDoubleClick={getSelectedRows}
// onRowSelectionModelChange={(newRowSelectionModel) => {
// setRowSelectionModel(newRowSelectionModel);
// }}
// rowSelectionModel={rowSelectionModel}
/>
</div>
</section>
);
};
export default LogManagemnet;
[ src / pages/ LogManagement / LogManagement.css ]
- datepicker라이브러리의 기능중에서 width부분의 조절이 잘 되지 않아 별도의 css 파일을 만들어서 코드를 추가해줬다.
/* react-datePicker의 태그의 width가 100프로 적용이 안되서 아래의 스타일을 별도로 입력 */
.customDatePickerWidth > div > div.react-datepicker__input-container input {
width: 100%;
}
Backend
[ index.js ]
- route 추가
app.use("/logManagement", require("./routes/logManagement"));
[ src / routes / logManagement.js ]
- db에서 있는 날짜형식은 moment의 format기능을 거쳐서 등록되었기 때문에 찾는 조건도 일치시켜준다.
- 프론트에서 받은 파라미터중 아이디에 대한 조건은 "$regex" 를 이용했는데, mysql의 like연산자의 기능과 비슷하다.
- history 컬렉션에서 찾는 조건은 시작날짜보다 크거나 같고, 종료날짜보다 작거나 같아야 되기에 각각 "$gte","$lte"를 사용했다.
- sort를 이용해서 내림차순 정렬했다. 몽고db문법에서는 내림차순은 -1, 오름차순은 1을 쓰면된다.
- 조회한 데이터를 frontend로 보내준다.
const express = require("express");
const History = require("../models/History");
const router = express.Router();
const moment = require("moment");
router.post("/", async (req, res, next) => {
console.log(req.body);
let textUserId = req.body.textUserId;
let selectedStartDate = "";
let selectedEndDate = "";
if (req.body.selectedStartDate !== "") {
selectedStartDate = moment(req.body.selectedStartDate).format(
"YYYY-MM-DD 00:00:00"
);
}
if (req.body.selectedEndDate !== "") {
selectedEndDate = moment(req.body.selectedEndDate).format(
"YYYY-MM-DD 24:59:59"
);
}
let userData = {};
console.log(selectedStartDate);
console.log(selectedEndDate);
try {
//$regex -> mysql의 like연산자
//$gte -> ~보다 크거나 같음
//$lte -> ~보다 작거나 같음
userData = await History.find({
userId: { $regex: textUserId },
loginDatetime: { $gte: selectedStartDate, $lte: selectedEndDate },
}).sort({ loginDatetime: -1 });
//sort -1내림차순, 1은 오름차순
console.log(userData);
return res.status(200).send(userData);
} catch (error) {
next(error);
}
});
module.exports = router;
Frontend
[ src / pages / LogManagement / index.tsx ]
- 백앤드에서 받은 데이터들은 mui grid를 사용해서 넣어준다.
- mui 그리드에 대한 설명은 지난 챕터에서 다룬적이 있기 때문에 생략한다. 아래 링크를 참고하면된다.
[MySchedule project] 11. 관리자 - 사용자관리 로직 mui x-data-grid와 mui modal 사용
관리자용 페이지중에서 사용자 데이터를 관리하는 페이지를 만들려고 한다. 먼저 db에 저장되있는 사용자 데이터들을 조회해 오고, 조회해온 데이터를 수정, 삭제하는 기능과 사용자를 등록하
devth.tistory.com
[ 결과화면 ]
- 로그관리에 대한 엑셀다운로드 기능도 추가해준다.
Backend
[ src / routes / excelDownload.js ]
- 백앤드 엑셀다운로드 최종코드
const express = require("express");
const router = express.Router();
const ExcelJS = require("exceljs");
const User = require("../models/User");
const History = require("../models/History");
const moment = require("moment");
//사용자 데이터 조회 후 엑셀로 다운로드
router.post("/excelUserDataSelect", async (req, res, next) => {
console.log("====== excelUserDataSelect");
console.log(req.body);
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("My Sheet"); //이름지정
worksheet.columns = [
//엑셀 구조 만들기
{ header: "아이디", key: "userId", width: 20 },
{ header: "이름", key: "userName", width: 20 },
{ header: "이메일", key: "userEmail", width: 20 },
{ header: "폰번호", key: "userPhone", width: 20 },
{ header: "로그인타입", key: "loginType", width: 20 },
];
let userData = [];
let userId = req.body.userId;
let userName = req.body.userName;
try {
//db에 있는 데이터 조회하기 start
userData = await User.find({
userId: { $regex: userId },
userName: { $regex: userName },
});
//console.log(userData);
//조회해온 사용자 데이터 엑셀에 저장
for (let index in userData) {
//console.log(index);
console.log(userData[index]);
//엑셀 데이터 추가
let loginType = "";
if (userData[index].loginType === 0) {
loginType = "일반로그인";
} else {
loginType = "소셜로그인";
}
worksheet.addRow({
userId: userData[index].userId,
userName: userData[index].userName,
userEmail: userData[index].userEmail,
userPhone: userData[index].userPhone,
loginType: loginType,
});
}
//db에 있는 데이터 조회하기 end
//날짜형식으로 엑셀 파일 이름에 붙이기
const currentDate = new Date();
const currentDayFormat =
currentDate.getFullYear() +
"-" +
(currentDate.getMonth() + 1) +
"-" +
currentDate.getDate();
//엑셀 데이터 저장
res.header("Access-Control-Expose-Headers", "Content-Disposition");
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
);
res.setHeader(
"Content-Disposition",
"attachment; filename=" + currentDayFormat + "excelfile.xlsx"
);
// 다운로드
await workbook.xlsx.write(res);
res.end();
/*
workbook.xlsx.write(res).then(function (data) {
res.end();
console.log("File write done........");
});
*/
} catch (error) {
console.error(error);
return;
}
});
//로그인 로고 데이터 조회 후 엑셀로 다운로드
router.post("/excelloginLogoDataSelect", async (req, res, next) => {
console.log("====== excelloginLogoDataSelect");
console.log(req.body);
// console.log(moment(req.body.selectedDate).format("YYYY-M-D"));
let selectedDate = moment(req.body.selectedDate).format("YYYY-MM-D");
console.log(selectedDate);
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("My Sheet"); //이름지정
worksheet.columns = [
//엑셀 구조 만들기
{ header: "아이디", key: "userId", width: 20 },
{ header: "로그인 시간", key: "loginDatetime", width: 20 },
{ header: "로그인타입", key: "loginType", width: 20 },
{ header: "로그인 성공유무", key: "loginYn", width: 20 },
];
let userData = [];
let userId = req.body.userId;
try {
//db에 있는 데이터 조회하기 start
historyLoginData = await History.find({
userId: { $regex: userId },
loginDatetime: { $regex: selectedDate },
});
//console.log(historyLoginData);
//조회해온 사용자 데이터 엑셀에 저장
for (let index in historyLoginData) {
//console.log(index);
console.log(historyLoginData[index]);
//엑셀 데이터 추가
let loginType = "";
if (historyLoginData[index].loginType === 0) {
loginType = "일반로그인";
} else {
loginType = "소셜로그인";
}
worksheet.addRow({
userId: historyLoginData[index].userId,
loginDatetime: historyLoginData[index].loginDatetime,
loginType: loginType,
loginYn: historyLoginData[index].loginYn,
});
}
//db에 있는 데이터 조회하기 end
//날짜형식으로 엑셀 파일 이름에 붙이기
const currentDate = new Date();
const currentDayFormat =
currentDate.getFullYear() +
"-" +
(currentDate.getMonth() + 1) +
"-" +
currentDate.getDate();
//엑셀 데이터 저장
res.header("Access-Control-Expose-Headers", "Content-Disposition");
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
);
res.setHeader(
"Content-Disposition",
"attachment; filename=" + currentDayFormat + "excelfile.xlsx"
);
// 다운로드
await workbook.xlsx.write(res);
res.end();
/*
workbook.xlsx.write(res).then(function (data) {
res.end();
console.log("File write done........");
});
*/
} catch (error) {
console.error(error);
return;
}
});
module.exports = router;
Frontend
[ src / pages / Excel / index.tsx ]
- 프론트엔드 엑셀다운로드 최종코드
import React, { ChangeEvent, useState } from "react";
// import axiosInstance from "../../utils/axios";
import axios from "axios";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
const ExcelDownload = () => {
//사용자 데이터 조회용 state
const [userData, setUserData] = useState({
userId: "",
userName: "",
});
//로그인기록 데이터 조회용 state
const [loginRecord, setLoginRecord] = useState({
userId: "",
});
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
const link = document.createElement("a"); //blob 객체 URL을 설정할 링크
//사용자 데이터조회 change함수
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const { name, value } = e.target;
setUserData((prevState) => ({
...prevState,
[name]: value,
}));
};
//로그인 로그 change함수
const handlelogoChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const { name, value } = e.target;
setLoginRecord((prevState) => ({
...prevState,
[name]: value,
}));
};
//사용자 데이터 조회 start
const userDataSelect = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
e.preventDefault();
try {
const body = {
...userData,
};
axios({
url: "http://localhost:4003/excelDownload/excelUserDataSelect",
method: "POST",
data: body,
responseType: "blob", //이부분이 있어야 엑셀 파일로 받을수 있다.
}).then((response) => {
console.log(response);
// 서버에서 전달 받은 데이터를 blob으로 변환
const blob = new Blob([response.data]);
// blob을 사용해 객체 URL 생성
const fileObjectUrl = window.URL.createObjectURL(blob);
//blob 객체 URL을 설정할 링크
//const link = document.createElement("a");
link.href = fileObjectUrl;
link.style.display = "none";
// 다운로드 파일의 이름을 추출하는 메서드 호출
link.download = downloadFilename(response);
document.body.appendChild(link); //링크 body에 추가함
//click 이벤트를 발생시키고, 파일 다운로드를 실행함
link.click();
link.remove(); // 다운로드가 끝난 리소스(객체 URL)를 해제
window.URL.revokeObjectURL(fileObjectUrl);
});
} catch (error) {
console.log(error);
}
// 엑셀 다운로드 파일 이름을 추출하는 함수
const downloadFilename = (response: any) => {
const disposition = response.headers["content-disposition"];
const fileName = decodeURI(
disposition
.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)[1]
.replace(/['"]/g, "")
);
console.log(fileName);
return fileName;
};
};
//사용자 데이터 조회 end
//로그인 로그 데이터 조회 start
const LogDataSelect = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
e.preventDefault();
try {
const body = {
...loginRecord,
selectedDate,
};
axios({
url: "http://localhost:4003/excelDownload/excelloginLogoDataSelect",
method: "POST",
data: body,
responseType: "blob", //이부분이 있어야 엑셀 파일로 받을수 있다.
}).then((response) => {
console.log(response);
// 서버에서 전달 받은 데이터를 blob으로 변환
const blob = new Blob([response.data]);
// blob을 사용해 객체 URL 생성
const fileObjectUrl = window.URL.createObjectURL(blob);
//blob 객체 URL을 설정할 링크
//const link = document.createElement("a");
link.href = fileObjectUrl;
link.style.display = "none";
// 다운로드 파일의 이름을 추출하는 메서드 호출
link.download = downloadFilename(response);
document.body.appendChild(link); //링크 body에 추가함
//click 이벤트를 발생시키고, 파일 다운로드를 실행함
link.click();
link.remove(); // 다운로드가 끝난 리소스(객체 URL)를 해제
window.URL.revokeObjectURL(fileObjectUrl);
});
} catch (error) {
console.log(error);
}
// 엑셀 다운로드 파일 이름을 추출하는 함수
const downloadFilename = (response: any) => {
const disposition = response.headers["content-disposition"];
const fileName = decodeURI(
disposition
.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)[1]
.replace(/['"]/g, "")
);
console.log(fileName);
return fileName;
};
};
//로그인 로그 데이터 조회 end
return (
<section className="w-full my-10">
<p className="my-4 text-sm text-gray-800">
{" "}
- 다운로드 버튼을 클릭하시면 엑셀 파일 형식으로 데이터를 보실수
있습니다.{" "}
</p>
<div className="grid sm:grid-cols-2 gap-4">
{/* 로그인 기록 조회 */}
<div className="py-4 border-[1px] rounded-md border-gray-200 text-center">
<div className="my-2">
<p className="text-lg font-semibold my-2">로그인 기록 조회</p>
</div>
<div className="my-4">
<label className="">아이디: </label>
<input
onChange={handlelogoChange}
id="userId"
name="userId"
value={loginRecord.userId}
className="w-[200px] border-b-[1px] border-gray-500 placeholder-gray-400 text-gray-700 text-md font-medium mt-2 h-[35px] p-2 focus:focus:outline-none"
/>
</div>
<div className="my-4">
<label className="">날짜: </label>
<DatePicker
className="w-[200px] border-b-[1px] border-gray-500 px-2"
dateFormat="yyyy.MM.dd" // 날짜 형태
shouldCloseOnSelect // 자동 닫힘 기능
minDate={new Date("2023-01-01")} // minDate 이전 날짜 선택 불가
//maxDate={new Date()} // maxDate 이후 날짜 선택 불가
selected={selectedDate}
onChange={(date) => setSelectedDate(date)}
/>
</div>
<div className="my-2">
<button
onClick={LogDataSelect}
className="w-5/12 my-2 bg-blue-700 text-gray-100 p-2 h-[40px] font-bold text-md py-2 px-2 rounded-md hover:ring hover:ring-blue-700"
>
Log_download
</button>
</div>
</div>
{/* 사용자 데이터 엑셀로 조회 */}
<div className="py-4 border-[1px] rounded-md border-gray-200 text-center">
<div className="my-4">
<p className="text-lg font-semibold my-2">사용자 데이터 조회</p>
</div>
<div className="">
<label className="">이름: </label>
<input
onChange={handleChange}
id="userName"
name="userName"
value={userData.userName}
className="w-[200px] border-b-[1px] border-gray-500 placeholder-gray-400 text-gray-700 text-md font-medium mt-2 h-[35px] p-2 focus:focus:outline-none"
/>
</div>
<div className="">
<label className="">아이디: </label>
<input
onChange={handleChange}
id="userId"
name="userId"
value={userData.userId}
className="w-[200px] border-b-[1px] border-gray-500 placeholder-gray-400 text-gray-700 text-md font-medium mt-2 h-[35px] p-2 focus:focus:outline-none"
/>
</div>
<div className="my-2">
<button
onClick={userDataSelect}
className="w-5/12 my-2 bg-blue-700 text-gray-100 p-2 h-[40px] font-bold text-md py-2 px-2 rounded-md hover:ring hover:ring-blue-700"
>
download
</button>
</div>
</div>
</div>
</section>
);
};
export default ExcelDownload;
'React + Node.js' 카테고리의 다른 글
[node.js / mysql] 1. node.js 초기 환경설정 (0) | 2024.02.29 |
---|---|
[MySchedule project] 19. header (Navbar) 모바일 반응형 / 툴팁 / 토글 사용 (1) | 2024.02.27 |
[MySchedule project] 18. exceljs를 사용한 엑셀 다운로드 구현 (0) | 2024.02.22 |
[MySchedule project] 17. styled components 사용하기 / Footer (0) | 2024.02.20 |
[MySchedule project] 16. React-big-Calendar 사용하기 (2) (0) | 2024.02.20 |