기획 단계에서 사용자가 NFT를 발행하고 앱을 사용을 중지하는 경우를 방지하기 위해 우선 운영진의 지갑에서 NFT를 발행하고, 사용자가 요청할 때 사용자의 지갑으로 NFT를 전송하도록 시스템을 설계했다.
이 때 사용자가 운영진의 지갑에 NFT를 스테이킹 하도록 혜택을 제공하기로 결정했다. 예를 들어 일정 기간 이상 NFT를 스테이킹하면 건강검진권이나 영양제를 증정하는 식이다.
이를 위해 현재 NFT를 스테이킹하는 사용자의 목록을 관리할 필요가 있다.
또한 사용자가 API를 호출할 때마다 로그를 저장해 어떤 기능을 가장 많이 사용하였는지 파악하면 차후 운영에 도움이 될 거라 예상하여 관리자 페이지에 대시보드를 추가하였다.
처음에 .csv 파일을 생성하여 다운로드 하는 방법은 쉬웠는데, .xlsx 등 엑셀 파일로 다운로드 하는 경우 조금 복잡하였다.
이 때 react-csv, react-xls 패키지를 사용하여 해결했다.
// users.tsx
import type { NextPage } from 'next'
import { gql, useQuery } from "@apollo/client";
import SessionStorage from "../utils/sessionStorage"
import Link from "next/link";
import { PageTitle } from "../components/PageTitle"
import { StyledTable, StyledTh, StyledTd, StyledTr, StyledNewButtonDiv, StyledNewButton } from "../components/StyledTable"
import { StyledLoadingGif } from "../components/StyledCommon"
import React from "react";
import { CSVLink } from "react-csv";
import moment from "moment"
import { DISEASE } from '../constants/disease';
import { useExcelDownloder } from 'react-xls';
// csv 파일 생성 시 사용할 헤더
const headers = [
{ label: "no.", key: "no"},
{ label: "email", key: "email"},
{ label: "이름", key: "nickname"},
{ label: "생년월일", key: "dateOfBirth"},
{ label: "포인트 잔액", key: "pointBalance"},
{ label: "가입 일자", key: "createdAt"},
{ label: "질환", key: "disease"},
{ label: "전화 번호", key: "phoneNumber"},
]
const GET_USERS = gql`
query GetUsers($jwt: String!, $nickname: String, $email: String) {
getUsers(jwt: $jwt, nickname: $nickname, email: $email) {
_id
email
nickname
dateOfBirth
pointBalance
createdAt
disease
phoneNumber
}
}
`
function getDiseaseKorean(disease:[]) {
let diseaseKorean = ""
for (let i = 0; i < disease.length; i++) {
diseaseKorean += DISEASE[i]
if(i !== (disease.length - 1)) {
diseaseKorean += ", "
}
}
return diseaseKorean;
}
const Users: NextPage = () => {
const { ExcelDownloder, Type } = useExcelDownloder();
const { loading, data } = useQuery(
GET_USERS,
{ variables: { jwt: SessionStorage.getItem("jwt") } }
);
if (loading) {
<StyledLoadingGif/>
}
if (data) {
// csv 파일 생성 시 사용할 데이터
var csvData = []
// 엑셀 파일 생성 시 사용할 라벨
var koreanLabelData = []
for (let i = 0; i< data.getUsers.length; i++) {
let user = data.getUsers[i]
let userCsvData = {
no: i+1,
email: user.email,
nickname: user.nickname,
dateOfBirth: user.dateOfBirth,
pointBalance: user.pointBalance,
createdAt: user.createdAt,
disease: getDiseaseKorean(user.disease),
phoneNumber:user.phoneNumber
}
csvData.push(userCsvData)
let labelData = {
"no.": i+1,
"email": user.email,
"이름": user.nickname,
"생년월일": user.dateOfBirth,
"포인트 잔액": user.pointBalance,
"가입 일자": user.createdAt,
"질환": getDiseaseKorean(user.disease),
"전화 번호":user.phoneNumber
}
koreanLabelData.push(labelData)
}
const xlsxData = {
users: koreanLabelData
};
return (
<div>
<PageTitle title="사용자 목록"/>
<StyledNewButtonDiv>
<ExcelDownloder data={xlsxData} filename={`${moment().format("yyyyMMDD")}_사용자목록`}>
<StyledNewButton style={{marginRight:10}}>.xlsx 다운로드</StyledNewButton>
</ExcelDownloder>
<CSVLink data={csvData} headers={headers} filename={ `${moment().format("yyyyMMDD")}_사용자목록.csv`}>
<StyledNewButton>.csv 다운로드</StyledNewButton>
</CSVLink>
</StyledNewButtonDiv>
<StyledTable>
<thead>
<StyledTr>
<StyledTh scope="col">닉네임</StyledTh>
<StyledTh scope="col">이메일</StyledTh>
<StyledTh scope="col">전화 번호</StyledTh>
<StyledTh scope="col">생년월일</StyledTh>
<StyledTh scope="col">리워드</StyledTh>
<StyledTh scope="col">질환</StyledTh>
<StyledTh scope="col">가입 일자</StyledTh>
</StyledTr>
</thead>
<tbody>
{
data.getUsers.map((data:any) => {
return (
<Link key={data._id} href={`/users/${data._id}`}>
<tr>
<StyledTd>{data.nickname}</StyledTd>
<StyledTd>{data.email}</StyledTd>
<StyledTd>{data.phoneNumber}</StyledTd>
<StyledTd>{data.dateOfBirth}</StyledTd>
<StyledTd>{data.pointBalance}</StyledTd>
<StyledTd>{getDiseaseKorean(data.disease)}</StyledTd>
<StyledTd>{data.createdAt}</StyledTd>
</tr>
</Link>
)
})
}
</tbody>
</StyledTable>
</div>
)
} else {
return (
<StyledLoadingGif/>
)
}
}
export default Users
import type { NextPage } from 'next'
import SessionStorage from "../utils/sessionStorage"
import { Bar } from "react-chartjs-2"
import { CHART_BACKGROUND_COLOR, CHART_BORDER_COLOR } from "../constants/color"
import { gql, useLazyQuery} from "@apollo/client";
import { Chart, CategoryScale, LinearScale, BarElement } from 'chart.js'
import { useState, useEffect } from "react";
import moment from "moment"
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import React from "react";
import { PageTitle } from "../components/PageTitle"
import styled from "styled-components"
const GET_LOGS_BY_CREATED_AT = gql`
query GetLogsByCreatedAt($jwt: String, $createdAt: String) {
getLogsByCreatedAt(jwt: $jwt, createdAt: $createdAt) {
_id
methodName
createdAt
count
}
}
`
const StyledDashboardCaption = styled.div`
display: flex;
margin-left: 10%;
`
const StyledDashboardTitle = styled.span`
font-size: 1.5em;
margin-bottom: 0.83em;
font-weight: bold;
width: 400px;
`
const StyledDashboard = styled.div`
width: 80%;
margin-left: 10%;
`
const Home: NextPage = () => {
Chart.register(CategoryScale, LinearScale, BarElement)
// 로그 데이터 조회 시 전송할 로그 생성 시간
const [createdAt, setCreatedAt] = useState(moment().format("YYYYMMDD"))
const [createdAtChartData, setCreatedAtChartData] = useState({
labels:[""],
datasets:[
{
label: 'dataset',
data: [0],
backgroundColor: CHART_BACKGROUND_COLOR,
borderColor: CHART_BORDER_COLOR,
borderWidth: 1
}
]
})
const [getCreatedAtNewData, {loading, error, data}] = useLazyQuery(GET_LOGS_BY_CREATED_AT, {
variables: {
jwt: SessionStorage.getItem("jwt"),
createdAt: createdAt
},
fetchPolicy: 'network-only',
nextFetchPolicy: 'cache-first'
});
useEffect(() => {
getCreatedAtNewData()
if(data !== null && data !== undefined) {
let methodName:string[] = []
let methodCountData:number[] = []
for(const methodLog of data.getLogsByCreatedAt) {
methodName.push(methodLog._id)
methodCountData.push(methodLog.count)
}
// chart data state로 저장하여 데이터 변경 시 차트 새로 로딩
setCreatedAtChartData({
labels:methodName,
datasets:[
{
label: 'dataset',
data: methodCountData,
backgroundColor: CHART_BACKGROUND_COLOR,
borderColor: CHART_BORDER_COLOR,
borderWidth: 1
}
]
}
)
methodCountData = []
methodName = []
}
}, [data])
return (
<div>
<main>
<PageTitle title="대시보드"/>
<div>
<StyledDashboardCaption>
<StyledDashboardTitle>일자 별 API 호출 현황</StyledDashboardTitle>
// datapicker에서 날짜 선택 시 해당 날짜에 해당하는 로그 조회, 차트 로딩
<DatePicker dateFormat="yyyy-MM-dd" selected={moment(createdAt, "YYYYMMDD").toDate()}
onChange={(date) => {setCreatedAt(moment(date).format("YYYYMMDD")); getCreatedAtNewData()}}
className="custom"
/>
</StyledDashboardCaption>
<StyledDashboard>
{
createdAtChartData !== null && createdAtChartData !== undefined ? <Bar data={createdAtChartData}/> :<div>데이터가 없습니다.</div>
}
</StyledDashboard>
</div>
</main>
</div>
)
}
export default Home