박권수

Merge branch 'web'

Showing 71 changed files with 3861 additions and 875 deletions
......@@ -5,10 +5,20 @@
"version": "0.2.0",
"configurations": [
{
"name": "flutter_application_1",
"cwd": "frontend\\flutter_application_1",
"request": "launch",
"type": "dart"
}
"name": "Attach",
"port": 9229,
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node"
},
{
"name": "Attach to Chrome",
"port": 9222,
"request": "attach",
"type": "pwa-chrome",
"webRoot": "${workspaceFolder}"
},
]
}
\ No newline at end of file
......
const Koa = require('koa');
const cors = require('@koa/cors');
const Router = require('koa-router');
const cors = require('@koa/cors');
const bodyparser = require('koa-bodyparser');
const Mongoose = require('mongoose');
const api = require('./src/api');
const updateMedicineInfo = require('./src/lib/UpdatingMedicineInfo');
const MqttServer = require('./src/util/MqttServer');
const BatchSystem = require('./src/util/Batch');
// const FCM = require('./src/util/FCM');
require('dotenv').config();
// eslint-disable-next-line no-undef
......@@ -23,10 +25,10 @@ Mongoose.connect(MONGO_URL, {
useCreateIndex : true
}).then(() => {
console.log('\x1b[1;32mMongo DB is connected : ', MONGO_URL, '\x1b[0m');
// updateMedicineInfo.updateMedicineInfo();
BatchSystem.updateMedicineData();
}).catch(e => {
console.log(e);
})
});
app.use(bodyparser());
router.use('/api', api.routes());
......@@ -36,4 +38,7 @@ app.use(router.routes()).use(router.allowedMethods());
app.listen(SERVER_PORT, () => {
console.log('\x1b[1;36mPORT : ', SERVER_PORT, 'is connected\x1b[0m');
MqttServer.on();
})
\ No newline at end of file
// FCM.initializeFCM();
BatchSystem.removeQrCode();
BatchSystem.pushNotifyByDosage();
});
\ No newline at end of file
......
This diff could not be displayed because it is too large.
......@@ -17,11 +17,16 @@
"author": "박권수",
"license": "ISC",
"dependencies": {
"@google-cloud/storage": "^5.14.2",
"@koa/cors": "^3.1.0",
"firebase-admin": "^9.11.1",
"google-auth-library": "^7.10.0",
"koa-body": "^4.2.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.33",
"mqtt": "^4.2.6",
"node-cron": "^3.0.0"
"node-cron": "^3.0.0",
"qrcode": "^1.4.4"
},
"devDependencies": {
"eslint": "^7.32.0"
......
/* eslint-disable no-undef */
//회원가입, 로그인 및 로그아웃에 관한 api
const User = require('../../models/user');
const Profile = require('../../models/profile');
const DoctorInfo = require('../../models/doctorInfo');
const Hub = require('../../models/hub');
const Bottle = require('../../models/bottle');
const PatientInfo = require('../../models/patientInfo');
const { uploadDoctorLicense } = require('../../util/GoogleCloudStorage');
const Joi = require('joi');
const jwt = require('jsonwebtoken');
const axios = require('axios');
exports.register = async(ctx) => {
......@@ -13,8 +17,9 @@ exports.register = async(ctx) => {
password,
passwordCheck,
userNm,
userAge,
birth,
contact,
deviceToken,
} = ctx.request.body;
const schema = Joi.object().keys({
......@@ -22,8 +27,9 @@ exports.register = async(ctx) => {
password : Joi.string().required(),
passwordCheck : Joi.string().required(),
userNm : Joi.string().required(),
userAge : Joi.number().required(),
birth : Joi.string().required(),
contact : Joi.string().required(),
deviceToken : Joi.string().allow(null),
});
const result = schema.validate(ctx.request.body);
......@@ -44,6 +50,15 @@ exports.register = async(ctx) => {
return;
}
const existContact = await Profile.findOne({ contact, useYn : 'Y' });
if(existContact) {
ctx.status = 409;
ctx.body = {
error : '이미 가입된 번호입니다.',
};
return;
}
const user = new User({
userId,
userTypeCd : 'NORMAL',
......@@ -55,8 +70,9 @@ exports.register = async(ctx) => {
const profile = new Profile({
userId,
userNm,
userAge,
birth,
contact,
deviceToken,
});
await user.save();
......@@ -66,22 +82,59 @@ exports.register = async(ctx) => {
};
exports.searchHospital = async ctx => {
const {
hospitalNm,
page,
} = ctx.query;
const pageSlice = 5;
const url = 'http://apis.data.go.kr/B551182/hospInfoService1/getHospBasisList1';
// eslint-disable-next-line no-undef
let queryParams = '?' + encodeURIComponent('ServiceKey') + '=' + process.env.SERVICE_KEY;
queryParams += '&' + encodeURIComponent('pageNo') + '=' + encodeURIComponent(page);
queryParams += '&' + encodeURIComponent('numOfRows') + '=' + encodeURIComponent(pageSlice);
queryParams += '&' + encodeURIComponent('yadmNm') + '=' + encodeURIComponent(hospitalNm);
const result = await axios.get(url + queryParams);
ctx.status = 200;
ctx.body = {
totalPage : Math.ceil(result.data.response.body.totalCount / pageSlice),
hospitalList : result.data.response.body.items.item,
};
};
//의사 회원가입
exports.doctorRegister = async ctx => {
const {
userId,
password,
passwordCheck,
info,
contact,
hospitalNm,
hospitalAddr,
doctorType,
doctorNm,
} = ctx.request.body;
const { doctorInfoFile } = ctx.request.files;
const schema = Joi.object().keys({
userId : Joi.string().email().max(50).required(),
password : Joi.string().required(),
passwordCheck : Joi.string().required(),
info : Joi.object().required(),
})
doctorInfoFile : Joi.object().required(),
});
const result = schema.validate(ctx.request.body);
const result = schema.validate({
userId,
password,
passwordCheck,
doctorInfoFile,
});
if(result.error || password !== passwordCheck) {
ctx.status = 400;
ctx.body = {
......@@ -114,6 +167,24 @@ exports.doctorRegister = async ctx => {
return;
}
const [fileName, filePath] = [doctorInfoFile.name, doctorInfoFile.path];
const doctorLicense = await uploadDoctorLicense({
userId,
fileName,
filePath,
});
const info = {
contact,
hospitalAddr,
hospitalNm,
doctorType,
doctorNm,
doctorLicense,
};
const doctor = new User({
userId,
userTypeCd : 'DOCTOR',
......@@ -128,21 +199,27 @@ exports.doctorRegister = async ctx => {
useYn : 'W',
});
doctor.save();
doctorInfo.save();
await doctorInfo.save();
await doctor.save();
ctx.status = 201;
}
/**
* 로컬 로그인
* @param {*} ctx
* @returns token
* http methods : POST
*/
exports.login = async(ctx) => {
const { userId, password } = ctx.request.body;
const { userId, password, deviceToken } = ctx.request.body;
const schema = Joi.object().keys({
userId : Joi.string().email().max(50).required(),
password : Joi.string().required()
})
password : Joi.string().required(),
deviceToken : Joi.string().allow(null),
});
const result = schema.validate(ctx.request.body);
if(result.error) {
......@@ -154,7 +231,7 @@ exports.login = async(ctx) => {
}
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd) {
if(!user || !user.userTypeCd || user.authTypeCd !== 'NORMAL') {
ctx.status = 401;
ctx.body = {
error : '존재하지 않는 회원입니다.',
......@@ -170,7 +247,6 @@ exports.login = async(ctx) => {
};
return;
}
if(user.useYn !== 'Y') {
ctx.status = 403;
ctx.body = {
......@@ -179,6 +255,16 @@ exports.login = async(ctx) => {
return;
}
//일반 유저의 deviceToken값이 바뀌면 업데이트한다 = 기기가 바뀌면
if(user.userTypeCd === 'NORMAL') {
const profile = await Profile.findByUserId(user.userId);
if(deviceToken && profile.deviceToken !== deviceToken) {
profile.updateDeviceToken(deviceToken);
await profile.save();
}
}
const token = await user.generateToken();
ctx.cookies.set('access_token', token, {
httpOnly : true,
......@@ -188,11 +274,206 @@ exports.login = async(ctx) => {
ctx.status = 200;
ctx.body = {
userTypeCd : user.userTypeCd,
token
token,
};
};
//social Register
exports.socialRegister = async ctx => {
const { socialType } = ctx.params;
const { accessToken, deviceToken } = ctx.request.body;
const verifyingToken =
socialType.toUpperCase() === 'GOOGLE' ? async () => {
//id_token
const result = jwt.decode(accessToken);
return {
userId : result.email,
userNm : result.name,
contact : `${result.email}_등록되지않은 번호`,
birth : '등록되지않음',
};
}
: socialType.toUpperCase() === 'NAVER' ? async () => {
const url = 'https://openapi.naver.com/v1/nid/me';
const result = await axios.get(url, {
headers : {
Authorization : `Bearer ${accessToken}`,
},
});
const { email, mobile, name, birthday, birthyear } = result.data.response;
return {
userId : email,
userNm : name,
contact : mobile,
birth : `${birthyear}-${birthday}`,
};
}
: socialType.toUpperCase() === 'KAKAO' ? async () => {
const url = 'https://kapi.kakao.com/v2/user/me';
const result = await axios.get(url, {
headers : {
Authorization : `Bearer ${accessToken}`,
'Content-type' : 'application/x-www-form-urlencoded;charset=utf-8',
},
});
const { email, profile } = result.data.kakao_account;
return {
userId : email,
userNm : profile.nickname,
contact : `${email}_등록되지않은 번호`,
birth : '등록되지않음',
};
} : () => null;
const verifyingInfo = await verifyingToken();
if(!verifyingInfo || !verifyingInfo.userId) {
ctx.status = 403;
ctx.body = {
error : '잘못된 요청',
};
return;
}
const { userId, userNm, birth, contact } = verifyingInfo;
const existUser = await User.findByUserId(userId);
if(existUser) {
ctx.status = 409;
ctx.body = {
error : '이미 가입된 회원',
};
return;
}
const existContact = await Profile.findOne({ contact, useYn : 'Y'});
if(existContact) {
ctx.status = 409;
ctx.body = {
error : '이미 가입된 번호',
};
return;
}
const user = new User({
userId,
hashedPassword : 'unnecessary',
authTypeCd : socialType.toUpperCase(),
useYn : 'Y',
});
const profile = new Profile({
userId,
userNm,
birth,
contact,
deviceToken,
});
await profile.save();
await user.save();
ctx.status = 201;
};
//social Login
exports.socialLogin = async ctx => {
const { socialType } = ctx.params;
const { accessToken, deviceToken, } = ctx.request.body;
const verifyingToken =
socialType.toUpperCase() === 'GOOGLE' ? async () => {
//id_token : google Login
const result = jwt.decode(accessToken);
return result.email;
}
: socialType.toUpperCase() === 'NAVER' ? async () => {
//naver Login
const url = 'https://openapi.naver.com/v1/nid/me';
const result = await axios.get(url, {
headers : {
Authorization : `Bearer ${accessToken}`,
},
});
return result.data.response.email;
}
: socialType.toUpperCase() === 'KAKAO' ? async () => {
//kakao Login
const url = 'https://kapi.kakao.com/v2/user/me';
const result = await axios.get(url, {
headers : {
Authorization : `Bearer ${accessToken}`,
'Content-type' : 'application/x-www-form-urlencoded;charset=utf-8',
},
});
return result.data.kakao_account.email;
} : () => null;
const userId = await verifyingToken();
if(!userId) {
ctx.status = 403;
ctx.body = {
error : '잘못된 요청입니다',
};
return;
}
const user = await User.findByUserId(userId);
if(!user || user.useYn !== 'Y') {
ctx.status = 404;
ctx.body = {
error : '존재하지 않는 회원입니다.',
};
return;
} else if (user.authTypeCd !== socialType.toUpperCase()) {
ctx.status = 400;
ctx.body = {
error : '잘못된 소셜 로그인입니다.',
};
return;
}
const profile = await Profile.findOne({ userId });
if(profile.deviceToken !== deviceToken) {
profile.updateDeviceToken(deviceToken);
await profile.save();
}
const token = await user.generateToken();
ctx.status = 200;
ctx.body = {
userTypeCd : user.userTypeCd,
token,
};
};
/**
* 로그아웃
* @param {*} ctx
* httm methods : POST
*/
exports.logout = async(ctx) => {
ctx.cookies.set('access_token', null, {
httpOnly : true,
......@@ -202,6 +483,70 @@ exports.logout = async(ctx) => {
ctx.status = 204;
};
/**
* 회원 탈퇴
* @param {*} ctx
* http methods : delete
*/
exports.secession = async ctx => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
ctx.status = 401;
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || user.useYn !== 'Y') {
ctx.status = 403;
return;
}
const { password } = ctx.query;
const isPasswordTrue = await user.checkPassword(password);
if(!isPasswordTrue) {
ctx.status = 401;
ctx.body = {
error : '비밀번호가 틀렸습니다.',
};
return;
}
if(user.userTypeCd === 'NORMAL') {
//프로필 삭제
await Profile.updateOne({ userId }, { useYn : 'N' });
//유저에 등록된 허브, 약병, 약병정보 전부 삭제
const hubList = await Hub.find({ userId });
await Promise.all(hubList.map(async hub => {
await Bottle.deleteMany({ hubId : hub.hubId });
}));
await Hub.deleteMany({ userId });
//환자 정보 삭제
await PatientInfo.updateMany({ patientId : userId, useYn : 'Y'}, { useYn : 'N' });
//유저 삭제
await user.setUseYn('N');
await user.save();
} else if (user.userTypeCd === 'DOCTOR') {
//의사 정보 및 환자 정보 삭제
await DoctorInfo.updateOne({ doctorId : userId }, { useYn : 'WS' });
await PatientInfo.updateMany({ doctorId : userId }, { useYn : 'WS' });
await user.setUseYn('WS');
await user.save();
}
ctx.status = 200;
};
exports.verifyToken = async(ctx) => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
......@@ -214,6 +559,7 @@ exports.verifyToken = async(ctx) => {
return;
}
// eslint-disable-next-line no-undef
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
ctx.status = 400;
......
const Router = require('koa-router')
const KoaBody = require('koa-body')({multipart : true});
const authCtrl = require('./auth.ctrl')
const auth = new Router()
/**
* 회원가입 (email type) : 환자 회원가입
* 로컬 회원가입 (email type) : 환자 회원가입
* url : http://localhost:4000/api/auth/register
* request parameter : userId, password, passwordCheck
* return : null
......@@ -12,28 +13,60 @@ const auth = new Router()
auth.post('/register', authCtrl.register)
/**
* 회원가입 (email type) : 의사 회원가입
* 병원 검색
* url : http://localhost:4000/api/auth/hospital
* request parameter : hospitalNm
* return : xml type data
*/
auth.get('/hospital', authCtrl.searchHospital);
/**
* 로컬 회원가입 (email type) : 의사 회원가입
* url : http://localhost:4000/api/auth/register/doctor
* request parameter : userId, password, passwordCheck, doctorInfo
* request parameter : userId, password, passwordCheck, doctorInfo(File)
* return : null
*/
auth.post('/register/doctor', authCtrl.doctorRegister)
auth.post('/register/doctor', KoaBody, authCtrl.doctorRegister)
/**
* 로그인 (email type)
* 로컬 로그인 (email type)
* url : http://localhost:4000/api/auth/login
* request parameter : userId, password
* return : userId
* return : token, userTypeCd
*/
auth.post('/login', authCtrl.login)
/**
* 소셜 회원가입(Google, Naver, Kakao)
* url : http://localhost:4000/api/auth/register/${socialType}
* request parameter : accessToken
* return : status
*/
auth.post('/register/social/:socialType', authCtrl.socialRegister);
/**
* 소셜 로그인(Google, Naver, Kakao)
* url : http://localhost:4000/api/auth/login/${socialType}
* request parameter
* return : token, userTypeCd
*/
auth.post('/login/social/:socialType', authCtrl.socialLogin);
/**
* 로그아웃
* url : http://localhost:4000/api/auth/logout
* request parameter : null
* return : null
*/
auth.post('/logout', authCtrl.logout)
auth.post('/logout', authCtrl.logout);
/**
* 회원 탈퇴
* url : http://localhost:4000/api/auth
* request parameter : password
* return : null
*/
auth.delete('/', authCtrl.secession);
/**
* 토큰이 유효한지 확인
......
/* eslint-disable no-undef */
//어플에서 약병 등록 및, 약병에 관한 정보 조회 = 여기서 mqtt통신으로 broker에 데이터를 요청한다.
const Bottle = require('../../models/bottle');
const Hub = require('../../models/hub');
const Medicine = require('../../models/medicine');
const User = require('../../models/user');
const DoctorInfo = require('../../models/doctorInfo');
const PatientInfo = require('../../models/patientInfo');
const TakeMedicineHist = require('../../models/takeMedicineHistory');
const BottleMedicine = require('../../models/bottleMedicine');
const Feedback = require('../../models/feedback');
const Mqtt = require('../../lib/MqttModule');
const Mqtt = require('../../util/MqttModule');
const jwt = require('jsonwebtoken');
//약병 등록
......@@ -19,6 +19,7 @@ exports.bottleConnect = async(ctx) => {
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
......@@ -26,7 +27,7 @@ exports.bottleConnect = async(ctx) => {
return;
}
const { bottleId, hubId } = ctx.request.body;
const { bottleId, hubId, bottleNm } = ctx.request.body;
const isExistBottle = await Bottle.findByBottleId(bottleId);
if(isExistBottle) {
......@@ -53,7 +54,8 @@ exports.bottleConnect = async(ctx) => {
const newBottle = new Bottle({
bottleId,
hubId
hubId,
bottleNm,
});
const client = await Mqtt.mqttOn(hosting);
......@@ -73,6 +75,7 @@ exports.bottleDisconnect = async(ctx) => {
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
......@@ -100,6 +103,7 @@ exports.bottleDisconnect = async(ctx) => {
const topic = 'bottle/' + bottleId + '/bts';
Mqtt.mqttUnsubscribe(client, topic);
await BottleMedicine.updateMany({ bottleId }, { useYn : 'N' });
await Bottle.deleteOne({ bottleId });
ctx.status = 204;
......@@ -114,6 +118,7 @@ exports.getBottleInfo = async(ctx) => {
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
......@@ -146,21 +151,29 @@ exports.getBottleInfo = async(ctx) => {
const bottleMedicine = await BottleMedicine.findOne({ bottleId, useYn : 'Y' });
if(bottleMedicine) {
const medicine = await Medicine.findOne({ medicineId : bottleMedicine.medicineId });
const doctorInfo = await DoctorInfo.findOne({ doctorId : bottleMedicine.doctorId });
const takeMedicineHist = await TakeMedicineHist
.find({ bmId : bottleMedicine._id })
.sort({ takeDate : 'desc' })
.populate('bmId');
.sort({ takeDate : 'desc' });
ctx.status = 200;
ctx.body = {
bottle,
medicine,
doctorInfo,
dailyDosage : bottleMedicine.dailyDosage,
totalDosage : bottleMedicine.totalDosage,
takeMedicineHist,
};
} else {
ctx.status = 200;
ctx.body = {
bottle,
medicine : null,
doctorInfo : null,
dailyDosage : null,
totalDosage : null,
takeMedicineHist : [],
}
}
......@@ -175,6 +188,7 @@ exports.getBottleFeedback = async ctx => {
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
......@@ -213,7 +227,9 @@ exports.getBottleFeedback = async ctx => {
.populate('bmId');
ctx.status = 200;
ctx.body = feedbackList;
ctx.body = {
feedbackList
};
} else {
ctx.status = 404;
ctx.body = {
......@@ -231,6 +247,7 @@ exports.setMedicine = async(ctx) => {
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
......@@ -239,7 +256,7 @@ exports.setMedicine = async(ctx) => {
}
const { bottleId } = ctx.params;
const { medicineId, dosage, doctorId } = ctx.request.body;
const { medicineId, doctorId, dailyDosage, totalDosage, } = ctx.request.body;
const bottle = await Bottle.findByBottleId(bottleId);
if(!bottle) {
......@@ -273,7 +290,8 @@ exports.setMedicine = async(ctx) => {
let bottleMedicine = new BottleMedicine({
bottleId,
medicineId,
dosage,
dailyDosage,
totalDosage,
});
if(doctorId !== undefined && doctorId !== null && doctorId !== '') {
......@@ -286,14 +304,109 @@ exports.setMedicine = async(ctx) => {
return;
}
bottleMedicine.setDoctorId(doctorId);
await bottleMedicine.setDoctorId(doctorId);
}
await BottleMedicine.updateMany({ bottleId }, { useYn : 'N '});
await bottleMedicine.save();
ctx.status = 200;
};
//약 무게 세팅
exports.setMedicineWeight = async ctx => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
ctx.status = 401;
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
ctx.status = 403;
return;
}
const { bottleId } = ctx.params;
bottleMedicine.save();
const bottle = await Bottle.findByBottleId(bottleId);
if(!bottle) {
ctx.status = 404;
ctx.body = {
error : '약병 찾을 수 없음.',
}
return;
}
const hub = await Hub.findByHubId(bottle.getHubId());
if(hub.getHub_UserId() !== userId) {
ctx.status = 403;
ctx.body = {
error : '해당 허브 권한 없음',
}
return;
}
//toDo : 약병에서 가져온 무게 데이터를 이용하여, bottleMedicine값을 갱신.
const client = await Mqtt.mqttOn(await hub.getHubHost());
const topic = 'bottle/' + bottleId + '/stb';
const message = 'weight';
await Mqtt.mqttPublishMessage(client, { topic, message });
// const bottleMedicine = await BottleMedicine.findOne({ bottleId, useYn : 'Y' });
// const { totalWeight, totalDosage } = bottleMedicine;
// if(totalDosage) bottleMedicine.setEachWeight(totalWeight / totalDosage);
ctx.status = 200;
};
//약병 이름 변경
exports.setBottleName = async ctx => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
ctx.status = 401;
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
ctx.status = 403;
return;
}
const { bottleId } = ctx.params;
const { bottleNm } = ctx.request.body;
const bottle = await Bottle.findByBottleId(bottleId);
if(!bottle) {
ctx.status = 404;
ctx.body = {
error : '약병 찾을 수 없음.',
}
return;
}
const hub = await Hub.findByHubId(bottle.getHubId());
if(hub.getHub_UserId() !== userId) {
ctx.status = 403;
ctx.body = {
error : '해당 허브 권한 없음',
}
return;
}
await bottle.setBottleNm(bottleNm);
await bottle.save();
ctx.status = 200;
};
// //비어있는 약병에 의사를 등록한다.
......@@ -344,6 +457,7 @@ exports.getHubsBottleList = async(ctx) => {
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
......@@ -385,6 +499,7 @@ exports.getAllBottleList = async ctx => {
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
......@@ -402,7 +517,7 @@ exports.getAllBottleList = async ctx => {
ctx.status = 200;
ctx.body = {
bottleList
bottleList,
};
};
\ No newline at end of file
......
......@@ -44,6 +44,22 @@ bottle.get('/feedback/:bottleId', bottleCtrl.getBottleFeedback);
bottle.patch('/:bottleId', bottleCtrl.setMedicine);
/**
* 약병에 등록된 약의 무게 갱신
* request parameter : bottleid
* url : http://localhost:4000/api/bottle/weight/:bottleId
* return : null
*/
bottle.patch('/weight/:bottleId', bottleCtrl.setMedicineWeight);
/**
* 약병 이름 변경
* request parameter : bottleid, bottleNm
* url : http://localhost:4000/api/bottle/name/:bottleId
* return : null
*/
bottle.patch('/name/:bottleId', bottleCtrl.setBottleName);
/**
* 비어있는 약병에 전담의 등록
* request parameter : bottleId, doctorId
* url : http://localhost:4000/api/bottle/doctor/:bottleId
......
......@@ -9,8 +9,14 @@ const Feedback = require('../../models/feedback');
const Hub = require('../../models/hub');
const PatientInfo = require('../../models/patientInfo');
const DoctorInfo = require('../../models/doctorInfo');
const PrescribeInfo = require('../../models/prescribeInfo');
const jwt = require('jsonwebtoken');
const { uploadQrCode, getQrCodeUrl } = require('../../util/GoogleCloudStorage');
const QrCodeUtil = require('../../util/QrCodeUtil');
const { sendPushMessage } = require('../../util/FCM');
/**
* 현재 로그인한 유저의 의사 정보를 가져온다
......@@ -150,7 +156,8 @@ exports.getPatientDetail = async ctx => {
bottleId : bottle.bottleId,
useYn : 'Y',
});
reqUserBmList.push(bm);
if(bm) reqUserBmList.push(bm);
}));
const bottleList = await Promise.all(reqUserBmList.map(async bottleMedicine => {
......@@ -280,7 +287,7 @@ exports.writeReqPatientReport = async ctx => {
}
await patientInfo.updateInfo(info);
patientInfo.save();
await patientInfo.save();
ctx.status = 200;
......@@ -335,13 +342,31 @@ exports.writeReqBottleFeedback = async ctx => {
doctorId : userId,
feedback,
});
newFeedback.save();
await newFeedback.save();
//feedback 알람 보내기
const hub = await Hub.findOne({ hubId : bottle.hubId });
const patientProfile = await Profile.findOne({ userId : hub.userId });
if(patientProfile) {
sendPushMessage({
deviceToken : patientProfile.deviceToken,
title : '의사에게 새로운 알람이 도착했습니다.',
body : feedback,
});
}
ctx.status = 200;
};
exports.searchPatientById = async ctx => {
/**
* 이메일로 환자를 검색한다.
* @param {*} ctx
* @returns
*/
exports.searchPatientByContact = async ctx => {
const token = ctx.req.headers.authorization;
if (!token || !token.length) {
ctx.status = 401;
......@@ -359,22 +384,19 @@ exports.searchPatientById = async ctx => {
return;
}
const { patientId } = ctx.params;
const patient = await User.findByUserId(patientId);
if(!patient || patient.useYn !== 'Y') {
ctx.status = 404;
ctx.body = {
error : '존재하지 않는 회원',
};
return;
}
const { contact } = ctx.params;
const patientProfile = await Profile.findOne({ contact, useYn : 'Y' });
const patientProfile = await Profile.findOne({ userId : patientId });
const patientInfo = {
userId : patientProfile.userId,
userNm : patientProfile.userNm,
birth : patientProfile.birth,
contact: patientProfile.contact,
};
ctx.status = 200;
ctx.body = {
patientNm : patientProfile.userNm,
patientId,
patientInfo,
};
};
......@@ -427,8 +449,17 @@ exports.registerNewPatient = async ctx => {
useYn : 'W',
});
patientInfo.updateInfo('환자 등록 요청');
patientInfo.save();
await patientInfo.updateInfo('환자 등록 요청');
await patientInfo.save();
const profile = await Profile.findByUserId(patientId);
const { deviceToken } = profile;
sendPushMessage({
deviceToken,
title : '새로운 의사 등록 요청이 왔습니다.',
body : '어플리케이션을 실행해 확인하세요.',
});
ctx.status = 200;
......@@ -477,8 +508,113 @@ exports.removeReqPatient = async ctx => {
}
await patientInfo.setUseYn('N')
patientInfo.save();
await patientInfo.save();
ctx.status = 200;
};
/**
* 특정 환자에게 특정 약을 처방한다
* @param {*} ctx
* http methods : post
*/
exports.prescribeMedicine = async ctx => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
ctx.status = 401;
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || user.userTypeCd !== 'DOCTOR') {
ctx.status = 403;
ctx.body = {
error : '권한 없는 유저',
};
return;
}
const {
patientId,
medicineId,
dailyDosage,
totalDosage,
} = ctx.request.body;
const patientInfo = await PatientInfo.findOne({
patientId,
doctorId : userId,
useYn : 'Y',
})
if(!patientInfo) {
ctx.status = 403;
ctx.body = {
error : '권한 없는 환자',
};
return;
}
const medicine = await Medicine.findOne({ medicineId });
if(!medicine) {
ctx.status = 404;
ctx.body = {
error : '존재하지 않는 약',
};
return;
}
const qrCodeResult = await QrCodeUtil.generateQrCode_prescribe({
medicine,
dailyDosage,
totalDosage,
patientId,
doctorId : userId,
});
if(!qrCodeResult) {
ctx.status = 400;
ctx.body = {
error : 'QR 코드 생성 에러',
};
return;
}
const qrCodeUrl = await uploadQrCode(qrCodeResult);
if(!qrCodeUrl) {
ctx.status = 400;
ctx.body = {
error : 'QR 코드 생성 후 서버 업로드 에러',
};
return;
}
const prescribeInfo = new PrescribeInfo({
doctorId : userId,
patientId,
dailyDosage,
totalDosage,
medicineId,
qrCodeUrl,
});
await prescribeInfo.save();
//특이사항에 처방기록 저장
await patientInfo.updateInfo(`${medicine.name}, 하루 ${dailyDosage}회분 처방, 총 ${totalDosage}회분 처방`);
await patientInfo.save();
const { qrCodeFileName } = qrCodeResult;
const qrCode = await getQrCodeUrl({ qrCodeFileName });
ctx.status = 200;
ctx.body = {
qrCode,
};
};
\ No newline at end of file
......
......@@ -7,7 +7,7 @@ const doctor = new Router();
/**
* 현재 로그인한 유저(의사)의 정보를 가져옴.
* request parameter : token
* url : http://localhost:4000/doctor/
* url : http://localhost:4000/api/doctor/
* return : doctor's Info
*/
doctor.get('/', doctorCtrl.getDoctorsInfo);
......@@ -15,7 +15,7 @@ doctor.get('/', doctorCtrl.getDoctorsInfo);
/**
* 현재 로그인한 유저(의사)의 관리 환자 목록을 가져옴
* request parameter
* url : http://localhost:4000/doctor/patient
* url : http://localhost:4000/api/doctor/patient
* return : patient List
*/
doctor.get('/patient', doctorCtrl.getPatientList);
......@@ -23,7 +23,7 @@ doctor.get('/patient', doctorCtrl.getPatientList);
/**
* 현재 로그인한 유저(의사)의 관리 환자 상세 정보를 가져옴
* request parameter : patient Id
* url : http://localhost:4000/doctor/patient/:patientId
* url : http://localhost:4000/api/doctor/patient/:patientId
* return : patient Detail
*/
doctor.get('/patient/:patientId', doctorCtrl.getPatientDetail);
......@@ -31,7 +31,7 @@ doctor.get('/patient/:patientId', doctorCtrl.getPatientDetail);
/**
* 현재 로그인한 유저(의사)의 관리 약병 상세 정보를 가져옴
* request parameter : bottle Id
* url : http://localhost:4000/doctor/bottle/:bottleId
* url : http://localhost:4000/api/doctor/bottle/:bottleId
* return : bottle Detail
*/
doctor.get('/bottle/:bottleId', doctorCtrl.getBottleDetail);
......@@ -39,7 +39,7 @@ doctor.get('/bottle/:bottleId', doctorCtrl.getBottleDetail);
/**
* 현재 로그인한 유저(의사)의 특정 관리 환자의 특이사항을 기록함
* request parameter : reqUserId, info
* url : http://localhost:4000/doctor/patient
* url : http://localhost:4000/api/doctor/patient
* return : null
*/
doctor.patch('/patient', doctorCtrl.writeReqPatientReport);
......@@ -47,7 +47,7 @@ doctor.patch('/patient', doctorCtrl.writeReqPatientReport);
/**
* 현재 로그인한 유저(의사)의 특정 관리 환자의 약병의 피드백을 등록함.
* request parameter : bottleId, fdbType, feedback
* url : http://localhost:4000/doctor/bottle
* url : http://localhost:4000/api/doctor/bottle
* return : null
*/
doctor.post('/bottle', doctorCtrl.writeReqBottleFeedback);
......@@ -55,15 +55,15 @@ doctor.post('/bottle', doctorCtrl.writeReqBottleFeedback);
/**
* 현재 로그인한 유저(의사)가 이메일로 유저를 검색함
* request parameter : patientId
* url : http://localhost:4000/api/doctor/patient/search/:patientId
* url : http://localhost:4000/api/api/doctor/patient/search/:patientId
* return : patient Info(simple)
*/
doctor.get('/patient/search/:patientId', doctorCtrl.searchPatientById);
doctor.get('/patient/search/:contact', doctorCtrl.searchPatientByContact);
/**
* 현재 로그인한 유저(의사)의 관리 환자를 등록함.
* request parameter : patientId
* url : http://localhost:4000/doctor/patient
* url : http://localhost:4000/api/doctor/patient
* return : null
*/
doctor.post('/patient', doctorCtrl.registerNewPatient);
......@@ -71,11 +71,18 @@ doctor.post('/patient', doctorCtrl.registerNewPatient);
/**
* 현재 로그인한 유저(의사)의 특정 관리 환자를 삭제함.
* request parameter : patientId
* url : http://localhost:4000/doctor/patient/:patientId
* url : http://localhost:4000/api/doctor/patient/:patientId
* return : null
*/
doctor.delete('/patient/:patientId', doctorCtrl.removeReqPatient);
/**
* 의사가 관리하는 환자에 대해 특정 약을 처방함
* request paramter : patientId, medicineId, dosage
* url : http://localhost:4000/api/doctor/prescribe
* return : null
*/
doctor.post('/prescribe', doctorCtrl.prescribeMedicine);
module.exports = doctor;
module.exports = doctor;
\ No newline at end of file
......
//허브(Mqtt Broker)등록 및 삭제
const Hub = require('../../models/hub');
const Bottle = require('../../models/bottle');
const User = require('../../models/user');
const Mqtt = require('../../lib/MqttModule');
const DataProcess = require('../../lib/DataProcess');
const Mqtt = require('../../util/MqttModule');
const DataProcess = require('../../util/DataProcess');
const jwt = require('jsonwebtoken');
const BottleMedicine = require('../../models/bottleMedicine');
//허브 연결
exports.hubConnect = async (ctx) => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
......@@ -20,7 +23,7 @@ exports.hubConnect = async (ctx) => {
return;
}
const { hubId, host, port } = ctx.request.body;
const { hubId, host, hubNm, } = ctx.request.body;
const isExistHub = await Hub.findByHubId(hubId);
if(isExistHub) {
......@@ -30,7 +33,7 @@ exports.hubConnect = async (ctx) => {
const hosting = {
host,
port
port : "1883",
};
Mqtt.mqttOn(hosting, DataProcess.dataPublish);
......@@ -38,7 +41,8 @@ exports.hubConnect = async (ctx) => {
const hub = new Hub({
hubId,
hosting,
userId
userId,
hubNm,
});
await hub.save();
......@@ -47,6 +51,49 @@ exports.hubConnect = async (ctx) => {
};
//허브 연결 해제
exports.hubDisconnect = async(ctx) => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
ctx.status = 401;
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || !user.userTypeCd || user.useYn !== 'Y') {
ctx.status = 403;
return;
}
const { hubId } = ctx.params;
const hub = await Hub.findByHubId(hubId);
if(!hub) {
ctx.status = 404;
return;
}
if(hub.getHub_UserId() !== userId) {
ctx.status = 403;
return;
}
const hosting = await hub.getHubHost();
Mqtt.mqttOff(hosting);
const bottleList = await Bottle.find({ hubId });
await Promise.all(bottleList.map(async bottle => {
await BottleMedicine.updateMany({ bottleId : bottle.bottleId }, { useYn : 'N' });
}));
await Bottle.deleteMany({ hubId });
await Hub.deleteOne({ hubId });
ctx.status = 204;
};
//허브 정보 조회
exports.getHubList = async(ctx) => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
......@@ -74,7 +121,8 @@ exports.getHubList = async(ctx) => {
};
};
exports.hubDisconnect = async(ctx) => {
//허브 이름 변경
exports.setHubName = async ctx => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
ctx.status = 401;
......@@ -90,21 +138,9 @@ exports.hubDisconnect = async(ctx) => {
}
const { hubId } = ctx.params;
const { hubNm } = ctx.request.body;
const hub = await Hub.findByHubId(hubId);
if(!hub) {
ctx.status = 404;
return;
}
if(hub.getHub_UserId() !== userId) {
ctx.status = 403;
return;
}
const hosting = await hub.getHubHost();
Mqtt.mqttOff(hosting);
await Hub.deleteOne({ hubId });
await Hub.updateOne({ hubId }, { hubNm });
ctx.status = 204;
ctx.status = 200;
};
......
......@@ -12,6 +12,14 @@ const hub = new Router();
hub.post('/', hubCtrl.hubConnect);
/**
* 허브 등록 해제
* request parameter : x
* url : http://localhost:4000/api/hub/:hubId
* return : null
*/
hub.delete('/:hubId', hubCtrl.hubDisconnect);
/**
* 로그인한 유저의 허브 목록 가져오기
* request parameter : X
* url : http://localhost:4000/api/hub
......@@ -20,11 +28,13 @@ hub.post('/', hubCtrl.hubConnect);
hub.get('/', hubCtrl.getHubList);
/**
* 허브 등록 해제
* request parameter : x
* 로그인한 유저의 특정 허브 이름 변경
* request parameter : hubId, hubNm
* url : http://localhost:4000/api/hub/:hubId
* return : null
*/
hub.delete('/:hubId', hubCtrl.hubDisconnect);
hub.patch('/:hubId', hubCtrl.setHubName);
module.exports = hub;
\ No newline at end of file
......
......@@ -7,6 +7,7 @@ const hub = require('./hub');
const medicine = require('./medicine');
const doctor = require('./doctor');
const manage = require('./manage');
const test = require('./test');
const api = new Router();
......@@ -17,5 +18,6 @@ api.use('/hub', hub.routes());
api.use('/medicine', medicine.routes());
api.use('/doctor', doctor.routes());
api.use('/manage', manage.routes());
api.use('/test', test.routes());
module.exports = api;
\ No newline at end of file
......
......@@ -12,6 +12,14 @@ const manage = new Router();
manage.get('/doctor', manageCtrl.getDoctorRegReqList);
/**
* 의사 회원탈퇴 요청을 한 회원들의 목록을 리턴
* request parameter : null
* url : http://localhost:4000/api/manage/doctor/sec
* return : doctor request List
*/
manage.get('/doctor/secession', manageCtrl.getDoctorSecReqList);
/**
* 의사 회원가입 요청을 한 특정 회원의 상세정보 확인
* request parameter : doctor Id
* url : http://localhost:4000/api/manage/doctor/:doctorId
......@@ -25,7 +33,7 @@ manage.get('/doctor/:doctorId', manageCtrl.getDoctorRegReqDetail);
* url : http://localhost:4000/api/manage/doctor/accept
* return : null
*/
manage.post('/doctor/accept', manageCtrl.acceptDoctorRegReq);
manage.patch('/doctor/accept', manageCtrl.acceptDoctorRegReq);
/**
* 의사 요청을 한 회원을 거절한다.
......@@ -33,7 +41,15 @@ manage.post('/doctor/accept', manageCtrl.acceptDoctorRegReq);
* url : http://localhost:4000/api/manange/doctor/reject
* return : null
*/
manage.post('/doctor/reject', manageCtrl.rejectDoctorRegReq);
manage.patch('/doctor/reject', manageCtrl.rejectDoctorRegReq);
/**
* 의사 탈퇴 요청을 수락한다.
* request parameter : doctor Id
* url : http://localhost:4000/api/manange/doctor/secession
* return : null
*/
manage.patch('/doctor/secession', manageCtrl.acceptDoctorSecReq);
/**
* 의사 요청을 한 회원의 자격 번호가 유효한지 검증한다
......
const User = require('../../models/user');
const DoctorInfo = require('../../models/doctorInfo');
const Profile = require('../../models/profile');
const PatientInfo = require('../../models/patientInfo');
const jwt = require('jsonwebtoken');
const { viewDoctorLicense } = require('../../util/GoogleCloudStorage');
/**
* 의사 회원가입을 요청한 회원 리스트를 확인한다.
......@@ -25,13 +27,54 @@ exports.getDoctorRegReqList = async ctx => {
}
try {
const doctorRegReqList = await DoctorInfo.find({
const doctorList = await DoctorInfo.find({
useYn : 'W',
});
ctx.status = 200;
ctx.body = {
doctorRegReqList
doctorList
};
} catch(e) {
ctx.status = 500;
ctx.body = {
error : '알 수 없는 에러가 발생했습니다.',
};
console.log(e);
return;
}
};
/**
* 의사 회원탈퇴를 요청한 회원 리스트를 확인한다.
* http methods : get
* @param {*} ctx
* @returns
*/
exports.getDoctorSecReqList = async ctx => {
const token = ctx.req.headers.authorization;
if (!token || !token.length) {
ctx.status = 401;
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || user.userTypeCd !== 'MANAGER' || user.useYn !== 'Y') {
ctx.status = 403;
return;
}
try {
const doctorList = await DoctorInfo.find({
useYn : 'WS',
});
ctx.status = 200;
ctx.body = {
doctorList
};
} catch(e) {
......@@ -40,6 +83,7 @@ exports.getDoctorRegReqList = async ctx => {
error : '알 수 없는 에러가 발생했습니다.',
};
console.log(e);
return;
}
};
......@@ -108,9 +152,20 @@ exports.getDoctorRegReqDetail = async ctx => {
return;
}
const doctorLicense = await viewDoctorLicense({
doctorInfo
});
ctx.status = 200;
ctx.body = {
doctorInfo,
doctorInfo : {
...doctorInfo._doc,
info : {
...doctorInfo.info,
doctorLicense,
},
},
};
} catch (e) {
......@@ -118,12 +173,14 @@ exports.getDoctorRegReqDetail = async ctx => {
ctx.body = {
error : '알 수 없는 에러가 발생했습니다.',
};
console.log(e);
return;
}
};
/**
* 의사 요청이 온 회원을 수락한다.
* http methods : post
* http methods : patch
* @param {*} ctx
* @returns
*/
......@@ -143,7 +200,7 @@ exports.acceptDoctorRegReq = async ctx => {
}
try {
const { doctorId } = ctx.request.body;
const { doctorId, validateDoctorLicense } = ctx.request.body;
const doctor = await User.findOne({ userId : doctorId });
if(!doctor) {
ctx.status = 404;
......@@ -169,17 +226,37 @@ exports.acceptDoctorRegReq = async ctx => {
error : '의사로 가입된 회원이 아닙니다.',
};
return;
} else if(!validateDoctorLicense) {
ctx.status = 400;
ctx.body = {
error : '유효한 자격 번호가 아닙니다.',
};
return;
}
const existDoctorInfo = await DoctorInfo.findOne({
'info.validateDoctorLicense' : validateDoctorLicense
});
if(existDoctorInfo) {
ctx.status = 403;
ctx.body = {
error : '중복된 자격번호입니다.',
};
return;
}
const doctorInfo = await DoctorInfo.findOne({
doctorId,
useYn : 'W',
});
doctor.setUseYn('Y');
doctor.save();
doctorInfo.setUseYn('Y');
doctorInfo.save();
await doctor.setUseYn('Y');
await doctor.save();
await doctorInfo.setUseYn('Y');
await doctorInfo.setValidateDoctorLicense(validateDoctorLicense);
await doctorInfo.save();
ctx.status = 200;
......@@ -189,12 +266,13 @@ exports.acceptDoctorRegReq = async ctx => {
error : '알 수 없는 에러가 발생했습니다.',
};
console.log(e);
return;
}
};
/**
* 의사 요청이 온 회원을 거절한다.
* http methods : post
* http methods : patch
* @param {*} ctx
* @returns
*/
......@@ -242,16 +320,79 @@ exports.acceptDoctorRegReq = async ctx => {
return;
}
await DoctorInfo.updateOne({ doctorId, useYn : 'W' }, { useYn : 'N' });
const doctorInfo = await DoctorInfo.findOne({
doctorId,
useYn : 'W',
});
await doctor.setUseYn('N');
await doctor.save();
doctor.setUseYn('N');
doctor.save();
doctorInfo.setUseYn('N');
doctorInfo.save();
ctx.status = 200;
} catch(e) {
ctx.status = 500;
ctx.body = {
error : '알 수 없는 에러가 발생했습니다.',
};
console.log(e);
return;
}
};
/**
* 의사 회원탈퇴 요청을 수락한다.
* http methods : patch
* @param {*} ctx
* @returns
*/
exports.acceptDoctorSecReq = async ctx => {
const token = ctx.req.headers.authorization;
if (!token || !token.length) {
ctx.status = 401;
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || user.userTypeCd !== 'MANAGER' || user.useYn !== 'Y') {
ctx.status = 403;
return;
}
try {
const { doctorId } = ctx.request.body;
const doctor = await User.findOne({ userId : doctorId });
if(!doctor) {
ctx.status = 404;
ctx.body = {
error : '존재하지 않는 회원입니다.',
};
return;
} else if(doctor.useYn === 'N') {
ctx.status = 400;
ctx.body = {
error : '탈퇴된 회원입니다.',
};
return;
} else if(doctor.useYn === 'Y') {
ctx.status = 400;
ctx.body = {
error : '이미 가입이 완료된 의사입니다.',
};
return;
} else if(doctor.userTypeCd !== 'DOCTOR') {
ctx.status = 400;
ctx.body = {
error : '의사로 가입된 회원이 아닙니다.',
};
return;
}
await DoctorInfo.updateOne({ doctorId, useYn : 'WS' }, { useYn : 'N' });
await PatientInfo.updateMany({ doctorId : userId, useYn : 'WS' }, { useYn : 'N' });
await doctor.setUseYn('N');
await doctor.save();
ctx.status = 200;
......@@ -261,9 +402,11 @@ exports.acceptDoctorRegReq = async ctx => {
error : '알 수 없는 에러가 발생했습니다.',
};
console.log(e);
return;
}
};
/**
* 회원가입을 요청한 의사의 유효한 자격 번호인지를 검증한다.
* @param {*} ctx
......@@ -284,12 +427,20 @@ exports.validateDoctorLicense = async ctx => {
return;
}
const { doctorLicense } = ctx.request.body;
const doctorInfo = await DoctorInfo.find({ 'info.doctorLicense' : doctorLicense });
const { validateDoctorLicense } = ctx.request.body;
if(!validateDoctorLicense) {
ctx.status = 404;
ctx.body = {
error : '유효한 자격번호를 입력해주세요',
};
return;
}
const doctorInfo = await DoctorInfo.findOne({ 'info.validateDoctorLicense' : validateDoctorLicense });
ctx.status = 200;
ctx.body = {
result : doctorInfo.length > 1 ? false : true,
result : doctorInfo ? false : true,
};
};
......
//test용 api입니다.
const Router = require('koa-router');
const testCtrl = require('./test.ctrl');
const test = new Router();
//푸쉬메시지 테스트
test.post('/fcm', testCtrl.sendFcmMessage);
module.exports = test;
//테스트용 api service
const { sendPushMessage } = require('../../util/FCM');
exports.sendFcmMessage = async ctx => {
const { deviceToken, title, body } = ctx.request.body;
try {
await sendPushMessage(ctx.request.body);
} catch(e) {
console.log('Error at FCM Sending : ', e);
}
};
\ No newline at end of file
......@@ -11,9 +11,18 @@ const user = new Router();
*/
user.get('/', userCtrl.getMyDetail);
/**
* 현재 로그인한 유저에 등록된 의사 목록 가져옴
* 현재 유저 정보 수정
* request parameter : token
* url : http://localhost:4000/api/user
* return : Object User
*/
user.patch('/', userCtrl.updateMyDetail);
/**
* 현재 로그인한 유저에 등록된 의사 목록 가져옴
* request parameter : userNm, birth, contact, password, passwordCheck
* url : http://localhost:4000/api/user/doctor
* return : Doctor List
*/
......
......@@ -38,9 +38,65 @@ exports.getMyDetail = async ctx => {
/**
* 내 정보를 업데이트한다.
* @param {*} ctx
* http methods : post
* http methods : patch
*/
exports.updateMyInfo = async ctx => {
exports.updateMyDetail = async ctx => {
const token = ctx.req.headers.authorization;
if(!token || !token.length) {
ctx.status = 401;
return;
}
// eslint-disable-next-line no-undef
const { userId } = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByUserId(userId);
if(!user || user.useYn !== 'Y' || user.userTypeCd !== 'NORMAL') {
ctx.status = 403;
return;
}
const profile = await Profile.findByUserId(userId);
if(!profile || profile.useYn !== 'Y') {
ctx.status = 403;
return;
}
const { userNm, birth, contact, password, passwordCheck, } = ctx.request.body;
const existContact = await Profile.findOne({ contact, useYn : 'Y' });
if(existContact) {
ctx.status = 409;
ctx.body = {
error : '이미 가입된 번호',
};
return;
}
//passwordCheck가 있고 로컬 회원이라면 비밀번호 변경함
if(passwordCheck && user.authTypeCd === 'NORMAL') {
//passwordCheck와 password가 같아야함
if(passwordCheck !== password) {
ctx.status = 401;
ctx.body = {
error : '비밀번호가 일치하지 않습니다.',
};
return;
}
await user.setPassword(password);
await user.save();
}
await profile.updateProfileInfo({
userNm,
birth,
contact,
});
await profile.save();
ctx.status = 200;
};
......@@ -76,7 +132,12 @@ exports.getMyDoctorList = async ctx => {
useYn : 'Y',
});
return doctorInfo.info;
if (doctorInfo) {
return ({
...doctorInfo.info,
doctorId : doctorInfo.doctorId,
})
}
}));
ctx.status = 200;
......@@ -112,7 +173,10 @@ exports.viewAllDoctorRegisterReq = async ctx => {
})
const doctorReqList = await Promise.all(patientInfoList.map(async patientInfo => {
const doctor = await DoctorInfo.findOne({ doctorId : patientInfo.doctorId });
const doctor = await DoctorInfo.findOne({
doctorId : patientInfo.doctorId,
useYn : 'Y',
});
return {
patientId : patientInfo.patientId,
doctorId : patientInfo.doctorId,
......@@ -156,9 +220,9 @@ exports.acceptDoctorRegister = async ctx => {
return;
}
patientInfo.updateInfo('환자 등록 요청 수락');
patientInfo.setUseYn('Y');
patientInfo.save();
await patientInfo.updateInfo('환자 등록 요청 수락');
await patientInfo.setUseYn('Y');
await patientInfo.save();
ctx.status = 200;
......
......@@ -11,6 +11,7 @@ exports.updateMedicineInfo = async() => {
//queryUrl을 return하는 함수 : 한 페이지에 100개의 item씩 요청할 수 있다.
const getQueryURL = (i) => {
const url = 'http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList';
// eslint-disable-next-line no-undef
const queryParams = '?' + encodeURIComponent('ServiceKey') + '=' + process.env.SERVICE_KEY;
const pageNum = '&' + encodeURIComponent('pageNo') + '=' + encodeURIComponent(i);
const numOfItem = '&' + encodeURIComponent('numOfRows') + '=' + encodeURIComponent(100);
......@@ -24,6 +25,7 @@ const getItemsList = async(queryUrl) => {
let i = 1, getItem = null, items = null;
const result = [];
// eslint-disable-next-line no-constant-condition
while(true) {
getItem = await axios.get(queryUrl(i));
items = getItem.data.body.items;
......
......@@ -8,6 +8,7 @@ const jwtMiddleware = async (ctx, next) => {
}
try {
// eslint-disable-next-line no-undef
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ctx.state.user = {
_id : decoded._id,
......
......@@ -5,6 +5,7 @@ const Schema = mongoose.Schema;
const BottleSchema = new Schema ({
bottleId : { type : Number, required : true, unique : true },
hubId : { type : Number, required : true, ref : 'Hub', },
bottleNm : { type : String, required : true, maxlength : 10, },
});
BottleSchema.statics.findByBottleId = function(bottleId) {
......@@ -23,5 +24,9 @@ BottleSchema.methods.getHubId = function() {
return this.hubId;
};
BottleSchema.methods.setBottleNm = function(bottleNm) {
this.bottleNm = bottleNm;
}
module.exports = mongoose.model('Bottle', BottleSchema);
\ No newline at end of file
......
......@@ -16,16 +16,26 @@ const BottleMedicineSchema = new Schema({
doctorId : {
type : String,
ref : 'User',
required : true,
lowercase : true,
},
dosage : {
dailyDosage : {
type : Number,
default : 1,
},
totalDosage : {
type : Number,
default : 1,
},
eachWeight : {
type : Number,
default : 0,
},
totalWeight : {
type : Number,
required : true,
default : 0,
},
regDtm : {
type : Date,
required : true,
default : Date.now,
},
useYn : {
......@@ -39,6 +49,14 @@ BottleMedicineSchema.methods.setDoctorId = function(doctorId) {
this.doctorId = doctorId;
};
BottleMedicineSchema.methods.setEachWeight = function(eachWeight) {
this.eachWeight = eachWeight;
};
BottleMedicineSchema.methods.setTotalWeight = function(totalWeight) {
this.totalWeight = totalWeight;
};
BottleMedicineSchema.methods.setUseYn = function(useYn) {
this.useYn = useYn;
};
......
......@@ -3,9 +3,10 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const DoctorInfoSchema = new Schema({
doctorId : { type : String, required : true, },
doctorId : { type : String, required : true, lowercase : true, },
info : {
doctorLicense : { type : String, required : true, },
validateDoctorLicense : { type : String, default : null },
hospitalNm : { type : String, default : null, },
hospitalAddr : { type : String, default : null, },
contact : { type : String, required : true, },
......@@ -23,5 +24,9 @@ DoctorInfoSchema.methods.setUseYn = function(useYn) {
this.useYn = useYn;
};
DoctorInfoSchema.methods.setValidateDoctorLicense = function(validateDoctorLicense) {
this.info.validateDoctorLicense = validateDoctorLicense;
};
module.exports = mongoose.model('DoctorInfo', DoctorInfoSchema);
......
......@@ -10,7 +10,7 @@ const FeedbackSchema = new Schema({
required : true,
ref : 'BottleMedicine',
},
doctorId : { type : String, required : true, ref : 'User', },
doctorId : { type : String, required : true, ref : 'User', lowercase : true, },
feedback : { type : String, required : true, },
});
......
......@@ -5,7 +5,8 @@ const Schema = mongoose.Schema;
const HubSchema = new Schema ({
hubId : { type : Number, required : true, unique : true },
hosting : { type : Object, default : null },
userId : { type : String, default : null, ref : 'User' },
userId : { type : String, default : null, ref : 'User', lowercase : true, },
hubNm : { type : String, required : true, maxlength : 10, },
});
HubSchema.statics.findByHubId = function(hubId) {
......
const mongoose = require('mongoose');
const moment = require('moment');
require('moment-timezone');
const Schema = mongoose.Schema;
const PatientInfoSchema = new Schema({
patientId : { type : String, required : true, ref : 'User', },
doctorId : { type : String, required : true, ref : 'User', },
patientId : { type : String, required : true, ref : 'User', lowercase : true, },
doctorId : { type : String, required : true, ref : 'User', lowercase : true, },
info : { type : String, required : true, },
useYn : { type : String, required : true, default : 'W', },
});
......@@ -35,9 +36,9 @@ PatientInfoSchema.methods.setUseYn = function(useYn) {
};
PatientInfoSchema.methods.updateInfo = function(info) {
const date = moment(new Date()).format('YYYY-MM-DD hh:mm');
const date = moment.tz('Asia/Seoul').format('YYYY-MM-DD HH:mm');
if(this.info.length)
this.info = this.info.concat('\n\n', `${date} ${info}`);
this.info = this.info.concat('\n\n', `${date} -> ${info}`);
else
this.info = `${date}${info}`;
};
......
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const PrescribeInfoSchema = new Schema({
doctorId : { type : String, require : true, },
patientId : { type : String, require : true, },
medicineId : { type : Number, require : true, },
dailyDosage : { type : Number, require : true, },
totalDosage : { type : Number, require : true, },
qrCodeUrl : { type : String, require : true, },
});
module.exports = mongoose.model('PrescribeInfo', PrescribeInfoSchema);
\ No newline at end of file
......@@ -3,10 +3,11 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ProfileSchema = new Schema({
userId : { type : String, required : true, ref : 'User', },
userId : { type : String, required : true, ref : 'User', lowercase : true, },
userNm : { type : String, required : true, },
userAge : { type : Number, required : true, },
birth : { type : String, required : true, },
contact : { type : String, required : true, },
useYn : { type : String, default : 'Y', },
deviceToken : { type : String, default : null, },
});
......@@ -14,12 +15,14 @@ ProfileSchema.statics.findByUserId = function(userId) {
return this.findOne({ userId });
};
ProfileSchema.methods.updateUserContact = function(contact) {
this.contact = contact;
ProfileSchema.methods.setUseYn = function(useYn) {
this.useYn = useYn;
};
ProfileSchema.methods.updateUserAge = function() {
this.userAge = this.userAge + 1;
ProfileSchema.methods.updateProfileInfo = function({ userNm, birth, contact }) {
if(userNm) { this.userNm = userNm }
if(birth) { this.birth = birth }
if(contact) { this.contact = contact }
};
ProfileSchema.methods.updateDeviceToken = function(deviceToken) {
......
......@@ -5,7 +5,6 @@ const Schema = mongoose.Schema;
const TakeMedicineHistorySchema = new Schema ({
takeDate : {
type : Date,
required : true,
default : Date.now,
},
bmId : {
......@@ -13,9 +12,10 @@ const TakeMedicineHistorySchema = new Schema ({
ref : 'BottleMedicine',
required : true,
},
temperature : { type : Number, default : 0 },
humidity : { type : Number, default : 0 },
balance : { type : Number, default : 0 },
temperature : { type : Number, default : 0, },
humidity : { type : Number, default : 0, },
dosage : { type : Number, default : 0, },
balance : { type : Number, default : 0, },
});
......
......@@ -5,9 +5,10 @@ const jwt = require('jsonwebtoken');
const Schema = mongoose.Schema;
const UserSchema = new Schema ({
userId : { type: String, required : true, unique : true, lowercase : true, },
hashedPassword : { type : String, required : true },
userTypeCd : { type : String, required : true, default : 'NORMAL' },
userId : { type: String, required : true, unique : true, lowercase : true, trim : true },
hashedPassword : { type : String, required : true, },
userTypeCd : { type : String, required : true, default : 'NORMAL', uppercase : true, },
authTypeCd : { type : String, required : true, default : 'NORMAL', uppercase : true, },
useYn : { type : String, default : 'W', required : true, },
});
......
......@@ -3,11 +3,136 @@
* 21/09/14
* Author : 박권수
* 배치 시스템
* 1) 매년 지나면 프로필의 Age를 +1
* 2) Dosage에 따라, Push Notification 발송
* 1) Dosage에 따라, Push Notification 발송
*/
const cron = require('node-cron');
const fs = require('fs');
const Profile = require('../models/profile');
const User = require('../models/user');
const Hub = require('../models/hub');
const Bottle = require('../models/bottle');
const BottleMedicine = require('../models/bottleMedicine');
const Medicine = require('../models/medicine');
const updateMedicineInfo = require('../lib/UpdatingMedicineInfo');
const { sendPushMessage } = require('./FCM');
//매월 1일 0시 0분에 약 정보 업데이트
exports.updateMedicineData = async () => {
cron.schedule('0 0 0 1 * *', () => {
updateMedicineInfo.updateMedicineInfo();
}, {
timezone : 'Asia/Tokyo',
});
};
//매주 일요일마다 불필요한 qrcode 제거
exports.removeQrCode = () => {
cron.schedule('0 0 0 * * 0', () => {
// eslint-disable-next-line no-undef
const qrDir = process.env.QR_DIR;
fs.rm(qrDir, { recursive : true, force : true, }, () => {
fs.mkdir(qrDir, (err) => { if(err) console.log(err) });
});
}, {
timezone : 'Asia/Tokyo',
});
};
//dosage에 따라, Push Notification을 발송한다.
//아침 8시, 점심 12시, 저녁 6시에 한번씩 발송
exports.pushNotifyByDosage = async() => {
//매일 아침 8시 : 복용량 상관 없이 보냄
cron.schedule('0 0 8 * * *', async () => {
const bottleMedicineList = await BottleMedicine.find({ useYn : 'Y', dosage : { $gte : 1 } });
bottleMedicineList.forEach(async bottleMedicine => {
const bottle = await Bottle.findOne({ bottleId : bottleMedicine.bottleId });
const hub = await Hub.findOne({ hubId : bottle.hubId });
const user = await User.findOne({ userId : hub.userId, useYn : 'Y' });
if(user) {
const profile = await Profile.findOne({ userId : user.userId });
const { deviceToken } = profile;
if(deviceToken) {
const medicine = await Medicine.findOne({ medicineId : bottleMedicine.medicineId });
pushNotify({
deviceToken,
title : '약 복용 시간입니다',
body : medicine.name + '을 복용하셔야 합니다.',
});
}
}
});
}, {
timezone : 'Asia/Tokyo',
});
//매일 점심 12시 : 복용량이 3인 환자들만
cron.schedule('0 0 12 * * *', async () => {
const bottleMedicineList = await BottleMedicine.find({ useYn : 'Y', dosage : { $gte : 3 } });
bottleMedicineList.forEach(async bottleMedicine => {
const bottle = await Bottle.findOne({ bottleId : bottleMedicine.bottleId });
const hub = await Hub.findOne({ hubId : bottle.hubId });
const user = await User.findOne({ userId : hub.userId, useYn : 'Y' });
if(user) {
const profile = await Profile.findOne({ userId : user.userId });
const { deviceToken } = profile;
if(deviceToken) {
const medicine = await Medicine.findOne({ medicineId : bottleMedicine.medicineId });
pushNotify({
deviceToken,
title : '약 복용 시간입니다',
body : medicine.name + '을 복용하셔야 합니다.',
});
}
}
});
}, {
timezone : 'Asia/Tokyo',
});
//매일 저녁 6시
cron.schedule('0 0 18 * * *', async () => {
const bottleMedicineList = await BottleMedicine.find({ useYn : 'Y', dosage : { $gte : 2 } });
bottleMedicineList.forEach(async bottleMedicine => {
const bottle = await Bottle.findOne({ bottleId : bottleMedicine.bottleId });
const hub = await Hub.findOne({ hubId : bottle.hubId });
const user = await User.findOne({ userId : hub.userId, useYn : 'Y' });
if(user) {
const profile = await Profile.findOne({ userId : user.userId });
const { deviceToken } = profile;
if(deviceToken) {
const medicine = await Medicine.findOne({ medicineId : bottleMedicine.medicineId });
pushNotify({
deviceToken,
title : '약 복용 시간입니다',
body : medicine.name + '을 복용하셔야 합니다.',
});
}
}
});
}, {
timezone : 'Asia/Tokyo',
});
};
const pushNotify = ({ deviceToken, title, body }) => {
//toDo : deviceToken을 받아서 push notification을 발송하는 함수
sendPushMessage({
deviceToken,
title,
body,
});
};
......
const Bottle = require('../models/bottle');
const BottleMedicine = require('../models/bottleMedicine');
const TakeMedicineHist = require('../models/takeMedicineHistory');
//message subscribe 후 message를 가공한 이후 해당 데이터를 보낼 topic과 message를 리턴하는 함수
exports.dataPublish = async (topic, message) => {
if(message.includes('weight')) {
//무게 갱신
const result = await updateBottleMedicineWeight(topic, message);
return result;
} else {
//client가 subscribe를 하면 메시지를 보낸 약병의 topic과 message를 가공 및 보낸 약병의 bottleId를 가져옴
const data = await factoring(topic, message);
//가공된 데이터를 bottleId의 약병에 업데이트
......@@ -13,68 +20,59 @@ exports.dataPublish = async (topic, message) => {
return result;
}
};
//Hub topic : bottle/bottleId
//Hub로부터 받은 message : 개폐여부/온도/습도/초음파센서
//Hub로부터 받은 message : 개폐여부/온도/습도/무게센서
const factoring = async (topic, message) => {
const bottleId = parseInt(topic.split('/')[1]);
const data = message.split('/');
let [isOpen, temperature, humidity, balance] = data;
if(isOpen === '0')
balance = await balanceFactoring(balance);
else balance = '-1';
const [isOpen, temperature, totalWeight, humidity] = data;
return {
bottleId,
isOpen,
openDate : new Date(),
temperature,
humidity,
balance
totalWeight,
};
}
const balanceFactoring = (balance) => {
const max = 10; //Digital Lead Sensor Maximum Value
const slicingBalance = max / 5;
if(parseInt(balance) < slicingBalance || parseInt(balance) > max * 2)
return '80';
else if(parseInt(balance) < slicingBalance * 2)
return '60';
else if(parseInt(balance) < slicingBalance * 3)
return '40';
else if(parseInt(balance) < slicingBalance * 4)
return '20';
else return '0';
}
//bottleId가 포함된 data를 받아서 해당 약병의 data를 업데이트한다.
const bottleInfoUpdate = async(data) => {
let { bottleId, isOpen, openDate, temperature, humidity, balance } = data;
let { bottleId, isOpen, temperature, humidity, totalWeight } = data;
if(!parseInt(isOpen)) {
bottleId = parseInt(bottleId);
isOpen = parseInt(isOpen);
temperature = parseFloat(temperature);
humidity = parseFloat(humidity);
balance = parseInt(balance);
totalWeight = parseFloat(totalWeight);
const bottleMedicine = await BottleMedicine.findOne({ bottleId, useYn : 'Y' });
if(bottleMedicine) {
if(isOpen) {
const lastTotalWeight = parseFloat(bottleMedicine.totalWeight);
const { eachWeight } = bottleMedicine;
const dosage = Math.round((lastTotalWeight - totalWeight) / parseFloat(eachWeight));
if(dosage > 0) {
const balance = Math.round(totalWeight / parseFloat(eachWeight));
const takeMedicineHist = new TakeMedicineHist({
takeDate : openDate,
bmId : bottleMedicine._id,
temperature,
humidity,
dosage,
balance,
});
takeMedicineHist.save();
await takeMedicineHist.save();
}
await bottleMedicine.setTotalWeight(totalWeight);
await bottleMedicine.save();
}
}
}
......@@ -84,11 +82,13 @@ const transPublishingTopicAndMessage = async(bottleId) => {
const topic = 'bottle/' + bottleId + '/stb';
const bottleMedicine = await BottleMedicine.findOne({ bottleId, useYn : 'Y' });
const takeMedicineHist = await TakeMedicineHist.find({
const takeMedicineHistList = await TakeMedicineHist.find({
bmId : bottleMedicine._id
}).sort((a, b) => a.takeDate < b.takeDate)[0];
}).sort({ takeDate : 'desc' }).limit(1);
const message = 'res/' + await transDate(takeMedicineHist.takeDate) + '/' + bottleMedicine.dosage;
const message = takeMedicineHistList && takeMedicineHistList[0] ?
'res/' + await transDate(takeMedicineHistList[0].takeDate) + '/' + takeMedicineHistList[0].dosage :
'res/' + await transDate(new Date()) + '/' + 0;
return {
topic,
......@@ -100,4 +100,23 @@ const transPublishingTopicAndMessage = async(bottleId) => {
const transDate = (date) => {
return (date.getMonth() + 1 < 10 ? '0' + String(date.getMonth() + 1) : String(date.getMonth() + 1))
+ (date.getDate() < 10 ? '0' + String(date.getDate()) : String(date.getDate()));
}
\ No newline at end of file
};
//무게센서를 이용하여 데이터값을 갱신하는 함수
const updateBottleMedicineWeight = async (topic, message) => {
const bottleId = parseInt(topic.split('/')[1]);
//message = weight/무게
const totalWeight = parseFloat(message.split('/')[1]);
const bottleMedicine = await BottleMedicine.findOne({ bottleId, useYn : 'Y' });
const totalDosage = parseInt(bottleMedicine.totalDosage);
//받은 값으로 총 무게를 설정한 이후, 총 무게 / 총 복용량으로 개별 무게를 설정한다.
await bottleMedicine.setTotalWeight(totalWeight);
await bottleMedicine.setEachWeight(totalWeight / totalDosage);
await bottleMedicine.save();
return null;
};
\ No newline at end of file
......
// const fcm = require('firebase-admin');
const axios = require('axios');
// exports.initializeFCM = () => {
// fcm.initializeApp({
// credential: fcm.credential.applicationDefault(),
// });
// };
exports.sendPushMessage = async ({ deviceToken, title, body }) => {
// const notifyMessage = {
// notification : {
// title,
// body,
// },
// token : deviceToken,
// };
// fcm.messaging().send(notifyMessage).then(res => {
// console.log(res);
// }).catch(e => {
// console.log(e);
// });
const message = {
to : deviceToken,
notification : {
title,
body,
priority : 'high',
},
data : null,
}
const url = 'https://fcm.googleapis.com/fcm/send';
const result = await axios.post(url, message, {
headers : {
'Content-Type' : 'application/json',
// eslint-disable-next-line no-undef
'Authorization' : `key=${process.env.FCM_KEY}`,
},
});
console.log(result.data);
};
\ No newline at end of file
const { Storage } = require('@google-cloud/storage');
const fs = require('fs');
const storage = new Storage();
const GoogleStorageUrl = 'https://storage.googleapis.com/';
//의사 아이디, 업로드할 파일명, 업로드할 파일 경로를 인자로 받아, File을 GCS에 업로드 후 GCS 주소를 반환
exports.uploadDoctorLicense = async ({ userId, fileName, filePath }) => {
const destination = userId + '_' + fileName;
try {
const result = await storage.bucket('doctor-info').upload(filePath, {
destination,
});
const doctorLicenseUrl = GoogleStorageUrl + `${result[0].bucket.id}/${result[0].name}`;
return doctorLicenseUrl;
} catch(e) {
console.log(e);
return null;
}
};
//의사 정보를 인자로 받아 해당 Doctor License의 Signed URL을 반환
exports.viewDoctorLicense = async ({ doctorInfo }) => {
try {
const fileName = doctorInfo.info.doctorLicense.split('/').pop();
const file = storage.bucket('doctor-info').file(fileName);
const option = {
version : 'v4',
expires : Date.now() + 1000 * 60 * 15,
action : 'read',
};
const [signedUrl] = file ? await file.getSignedUrl(option) : [null];
return signedUrl;
} catch(e) {
console.log(e);
return null;
}
};
//의사 ID, 약 ID, 복용량을 인자로 받아, QR Code를 생성
exports.uploadQrCode = async ({ directory, qrCodeFileName }) => {
const destination = qrCodeFileName;
try {
//파일을 GCS에 업로드
const result = await storage.bucket('prescribe-medicine-qrcode').upload(directory + '/' + qrCodeFileName, {
destination
});
//업로드 후 파일 삭제
fs.rm(directory + '/' + qrCodeFileName, () => {});
const qrCodeUrl = GoogleStorageUrl + `${result[0].bucket.id}/${result[0].name}`;
return qrCodeUrl;
} catch(e) {
console.log(e);
return null;
}
};
//생성된 QR코드의 signedUrl을 가져옴
exports.getQrCodeUrl = async ({ qrCodeFileName }) => {
try {
const fileName = qrCodeFileName;
const file = storage.bucket('prescribe-medicine-qrcode').file(fileName);
const option = {
version : 'v4',
expires : Date.now() + 1000 * 60 * 15,
action : 'read',
};
const [signedUrl] = file ? await file.getSignedUrl(option) : [null];
return signedUrl;
} catch(e) {
console.log(e);
return null;
}
};
\ No newline at end of file
const mqtt = require('mqtt')
const clientList = []
const mqtt = require('mqtt');
const clientList = [];
exports.mqttOn = async (hosting, foo) => {
const filterIndex = clientList.findIndex(client => {
......@@ -9,27 +9,29 @@ exports.mqttOn = async (hosting, foo) => {
})
if(filterIndex === -1) {
const client = mqtt.connect(hosting)
clientList.push(client)
const client = mqtt.connect(hosting);
clientList.push(client);
client.on('connect', () => {
console.log('Hub connected: ', client.connected)
})
});
client.on('message', async (topic, message) => {
const result = await foo(topic, message.toString())
const result = await foo(topic, message.toString());
console.log('\x1b[1;32msubscribe : topic', topic, 'message : ', message.toString(), '\x1b[0m')
this.mqttPublishMessage(client, result)
})
if(result) this.mqttPublishMessage(client, result);
});
return client
return client;
}
return clientList[filterIndex]
return clientList[filterIndex];
}
exports.mqttSubscribe = (client, topic) => {
client.subscribe(topic)
client.subscribe(topic, () => {
console.log('suscribe', topic);
});
}
exports.mqttPublishMessage = (client, { topic, message }) => {
......@@ -49,10 +51,10 @@ exports.mqttOff = (hosting) => {
return (client.options.clientId === hosting.clientId
&& client.options.host === hosting.host
&& client.options.port === hosting.port)
})
});
if(filterIndex !== -1) {
clientList[filterIndex].end()
clientList.splice(filterIndex, 1)
clientList[filterIndex].end();
clientList.splice(filterIndex, 1);
}
}
\ No newline at end of file
......
const Mqtt = require('../lib/MqttModule');
const DataProcess = require('../lib/DataProcess');
const Mqtt = require('./MqttModule');
const DataProcess = require('./DataProcess');
const Hub = require('../models/hub');
const Bottle = require('../models/bottle');
......
const QrCode = require('qrcode');
const moment = require('moment');
exports.generateQrCode_prescribe = async ({ medicine, dailyDosage, totalDosage, patientId, doctorId }) => {
// eslint-disable-next-line no-undef
const directory = process.env.QR_DIR;
const now = moment().format('YYYY-MM-DD_HH:mm');
const qrCodeFileName = `${now}_${doctorId}_${patientId}_${medicine.medicineId}_${dailyDosage}_${totalDosage}.png`;
try {
await QrCode.toFile(
directory + '/' + qrCodeFileName,
`${medicine.medicineId}/${dailyDosage}/${totalDosage}/${doctorId}/${patientId}/${medicine.name}`,
{
color : {
dark : '#337DFF',
light : '#FFF'
},
}
);
return {
directory,
qrCodeFileName,
};
} catch(e) {
console.log(e);
return null;
}
};
\ No newline at end of file
This diff could not be displayed because it is too large.
......@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
"name": "web",
"version": "0.1.0",
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
......@@ -18,10 +19,12 @@
"highcharts": "^9.2.0",
"highcharts-react-official": "^3.0.0",
"moment": "^2.29.1",
"qrcode": "^1.4.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-spinners": "^0.11.0",
"recoil": "^0.4.0",
"recoil-persist": "^3.0.0",
"styled-components": "^5.3.0",
......@@ -31,6 +34,7 @@
"web-vitals": "^1.0.1"
},
"devDependencies": {
"@types/qrcode": "^1.4.1",
"@types/react-router-dom": "^5.1.8",
"@types/styled-components": "^5.1.12",
"@types/validator": "^13.6.3",
......@@ -1247,11 +1251,14 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.12.18",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.18.tgz",
"integrity": "sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg==",
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz",
"integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime-corejs3": {
......@@ -1332,6 +1339,23 @@
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
},
"node_modules/@emotion/cache": {
"version": "11.4.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.4.0.tgz",
"integrity": "sha512-Zx70bjE7LErRO9OaZrhf22Qye1y4F7iDl+ITjet0J+i+B88PrAOBkKvaAWhxsZf72tDLajwCgfCjJ2dvH77C3g==",
"dependencies": {
"@emotion/memoize": "^0.7.4",
"@emotion/sheet": "^1.0.0",
"@emotion/utils": "^1.0.0",
"@emotion/weak-memoize": "^0.2.5",
"stylis": "^4.0.3"
}
},
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
},
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
......@@ -1345,6 +1369,49 @@
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
},
"node_modules/@emotion/react": {
"version": "11.4.1",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.4.1.tgz",
"integrity": "sha512-pRegcsuGYj4FCdZN6j5vqCALkNytdrKw3TZMekTzNXixRg4wkLsU5QEaBG5LC6l01Vppxlp7FE3aTHpIG5phLg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@emotion/cache": "^11.4.0",
"@emotion/serialize": "^1.0.2",
"@emotion/sheet": "^1.0.2",
"@emotion/utils": "^1.0.0",
"@emotion/weak-memoize": "^0.2.5",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"@babel/core": "^7.0.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz",
"integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==",
"dependencies": {
"@emotion/hash": "^0.8.0",
"@emotion/memoize": "^0.7.4",
"@emotion/unitless": "^0.7.5",
"@emotion/utils": "^1.0.0",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.2.tgz",
"integrity": "sha512-QQPB1B70JEVUHuNtzjHftMGv6eC3Y9wqavyarj4x4lg47RACkeSfNo5pxIOKizwS9AEFLohsqoaxGQj4p0vSIw=="
},
"node_modules/@emotion/stylis": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
......@@ -1355,6 +1422,16 @@
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"node_modules/@emotion/utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz",
"integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA=="
},
"node_modules/@emotion/weak-memoize": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
},
"node_modules/@eslint/eslintrc": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
......@@ -2229,17 +2306,6 @@
"node": ">=10"
}
},
"node_modules/@testing-library/dom/node_modules/@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@testing-library/dom/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
......@@ -2276,17 +2342,6 @@
"yarn": ">=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
......@@ -2315,17 +2370,6 @@
"react-dom": "*"
}
},
"node_modules/@testing-library/react/node_modules/@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@testing-library/user-event": {
"version": "12.8.3",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.8.3.tgz",
......@@ -2341,17 +2385,6 @@
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@testing-library/user-event/node_modules/@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@types/anymatch": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
......@@ -2536,6 +2569,15 @@
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
},
"node_modules/@types/qrcode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.4.1.tgz",
"integrity": "sha512-vxMyr7JM7tYPxu8vUE83NiosWX5DZieCyYeJRoOIg0pAkyofCBzknJ2ycUZkPGDFis2RS8GN/BeJLnRnAPxeCA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": {
"version": "17.0.16",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.16.tgz",
......@@ -4813,6 +4855,25 @@
"isarray": "^1.0.0"
}
},
"node_modules/buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"dependencies": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
}
},
"node_modules/buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
},
"node_modules/buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
},
"node_modules/buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
......@@ -6483,6 +6544,11 @@
"randombytes": "^2.0.0"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz",
"integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg=="
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
......@@ -12762,6 +12828,14 @@
"node": ">=6"
}
},
"node_modules/pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pnp-webpack-plugin": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
......@@ -14095,6 +14169,193 @@
"teleport": ">=0.2.0"
}
},
"node_modules/qrcode": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
"integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==",
"dependencies": {
"buffer": "^5.4.3",
"buffer-alloc": "^1.2.0",
"buffer-from": "^1.1.1",
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
"yargs": "^13.2.4"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=4"
}
},
"node_modules/qrcode/node_modules/ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/qrcode/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/qrcode/node_modules/cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dependencies": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"node_modules/qrcode/node_modules/emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
},
"node_modules/qrcode/node_modules/find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dependencies": {
"locate-path": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"engines": {
"node": ">=4"
}
},
"node_modules/qrcode/node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
},
"node_modules/qrcode/node_modules/locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dependencies": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dependencies": {
"p-limit": "^2.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dependencies": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dependencies": {
"ansi-regex": "^4.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dependencies": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dependencies": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"node_modules/qrcode/node_modules/yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
},
"node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
......@@ -14737,6 +14998,18 @@
"node": ">=0.10.0"
}
},
"node_modules/react-spinners": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.11.0.tgz",
"integrity": "sha512-rDZc0ABWn/M1OryboGsWVmIPg8uYWl0L35jPUhr40+Yg+syVPjeHwvnB7XWaRpaKus3M0cG9BiJA+ZB0dAwWyw==",
"dependencies": {
"@emotion/react": "^11.1.4"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0",
"react-dom": "^16.0.0 || ^17.0.0"
}
},
"node_modules/read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
......@@ -16776,6 +17049,11 @@
"node": ">=8"
}
},
"node_modules/stylis": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz",
"integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg=="
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
......@@ -20048,9 +20326,9 @@
}
},
"@babel/runtime": {
"version": "7.12.18",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.18.tgz",
"integrity": "sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg==",
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz",
"integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
......@@ -20124,6 +20402,23 @@
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
},
"@emotion/cache": {
"version": "11.4.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.4.0.tgz",
"integrity": "sha512-Zx70bjE7LErRO9OaZrhf22Qye1y4F7iDl+ITjet0J+i+B88PrAOBkKvaAWhxsZf72tDLajwCgfCjJ2dvH77C3g==",
"requires": {
"@emotion/memoize": "^0.7.4",
"@emotion/sheet": "^1.0.0",
"@emotion/utils": "^1.0.0",
"@emotion/weak-memoize": "^0.2.5",
"stylis": "^4.0.3"
}
},
"@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
},
"@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
......@@ -20137,6 +20432,37 @@
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
},
"@emotion/react": {
"version": "11.4.1",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.4.1.tgz",
"integrity": "sha512-pRegcsuGYj4FCdZN6j5vqCALkNytdrKw3TZMekTzNXixRg4wkLsU5QEaBG5LC6l01Vppxlp7FE3aTHpIG5phLg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@emotion/cache": "^11.4.0",
"@emotion/serialize": "^1.0.2",
"@emotion/sheet": "^1.0.2",
"@emotion/utils": "^1.0.0",
"@emotion/weak-memoize": "^0.2.5",
"hoist-non-react-statics": "^3.3.1"
}
},
"@emotion/serialize": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz",
"integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==",
"requires": {
"@emotion/hash": "^0.8.0",
"@emotion/memoize": "^0.7.4",
"@emotion/unitless": "^0.7.5",
"@emotion/utils": "^1.0.0",
"csstype": "^3.0.2"
}
},
"@emotion/sheet": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.2.tgz",
"integrity": "sha512-QQPB1B70JEVUHuNtzjHftMGv6eC3Y9wqavyarj4x4lg47RACkeSfNo5pxIOKizwS9AEFLohsqoaxGQj4p0vSIw=="
},
"@emotion/stylis": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
......@@ -20147,6 +20473,16 @@
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"@emotion/utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz",
"integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA=="
},
"@emotion/weak-memoize": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
},
"@eslint/eslintrc": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
......@@ -20777,14 +21113,6 @@
"pretty-format": "^26.6.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
......@@ -20812,14 +21140,6 @@
"redent": "^3.0.0"
},
"dependencies": {
"@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
......@@ -20838,16 +21158,6 @@
"requires": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^7.28.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
}
},
"@testing-library/user-event": {
......@@ -20856,16 +21166,6 @@
"integrity": "sha512-IR0iWbFkgd56Bu5ZI/ej8yQwrkCv8Qydx6RzwbKz9faXazR/+5tvYKsZQgyXJiwgpcva127YO6JcWy7YlCfofQ==",
"requires": {
"@babel/runtime": "^7.12.5"
},
"dependencies": {
"@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
}
},
"@types/anymatch": {
......@@ -21056,6 +21356,15 @@
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
},
"@types/qrcode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.4.1.tgz",
"integrity": "sha512-vxMyr7JM7tYPxu8vUE83NiosWX5DZieCyYeJRoOIg0pAkyofCBzknJ2ycUZkPGDFis2RS8GN/BeJLnRnAPxeCA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/react": {
"version": "17.0.16",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.16.tgz",
......@@ -22917,6 +23226,25 @@
"isarray": "^1.0.0"
}
},
"buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"requires": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
}
},
"buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
},
"buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
......@@ -24256,6 +24584,11 @@
"randombytes": "^2.0.0"
}
},
"dijkstrajs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz",
"integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg=="
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
......@@ -29110,6 +29443,11 @@
}
}
},
"pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="
},
"pnp-webpack-plugin": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
......@@ -30180,6 +30518,148 @@
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
},
"qrcode": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
"integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==",
"requires": {
"buffer": "^5.4.3",
"buffer-alloc": "^1.2.0",
"buffer-from": "^1.1.1",
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
"yargs": "^13.2.4"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"requires": {
"locate-path": "^3.0.0"
}
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"requires": {
"p-limit": "^2.0.0"
}
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
......@@ -30669,6 +31149,14 @@
}
}
},
"react-spinners": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.11.0.tgz",
"integrity": "sha512-rDZc0ABWn/M1OryboGsWVmIPg8uYWl0L35jPUhr40+Yg+syVPjeHwvnB7XWaRpaKus3M0cG9BiJA+ZB0dAwWyw==",
"requires": {
"@emotion/react": "^11.1.4"
}
},
"read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
......@@ -32283,6 +32771,11 @@
}
}
},
"stylis": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz",
"integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
......
......@@ -14,6 +14,7 @@
"highcharts": "^9.2.0",
"highcharts-react-official": "^3.0.0",
"moment": "^2.29.1",
"qrcode": "^1.4.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
......@@ -52,6 +53,7 @@
]
},
"devDependencies": {
"@types/qrcode": "^1.4.1",
"@types/react-router-dom": "^5.1.8",
"@types/styled-components": "^5.1.12",
"@types/validator": "^13.6.3",
......
import { client } from "./client";
export default {
register : (Data : any) => {
register : (Data : FormData) => {
return client.post('/auth/register', Data);
},
searchHospital : (hospitalNm : string, page : number) => {
return client.get('/auth/hospital', {
params : {
hospitalNm,
page,
},
});
},
registerDoctor : (Data : any) => {
return client.post('/auth/register/doctor', Data);
},
......
......@@ -44,8 +44,8 @@ export default {
},
});
},
searchPatientById : (token : RecoilState<any>, patientId : string) => {
return client.get(`/doctor/patient/search/${patientId}`, {
searchPatientByContact : (token : RecoilState<any>, contact : string) => {
return client.get(`/doctor/patient/search/${contact}`, {
headers : {
Authorization : token,
},
......@@ -65,4 +65,11 @@ export default {
},
});
},
prescribeMedicine : (token : RecoilState<any>, Data : any) => {
return client.post('/doctor/prescribe', Data, {
headers : {
Authorization : token,
},
});
},
};
\ No newline at end of file
......
......@@ -9,6 +9,13 @@ export default {
},
});
},
getDoctorSecReqList : (token : RecoilState<any>) => {
return client.get('/manage/doctor/secession', {
headers : {
Authorization : token,
},
});
},
getDoctorRegReqDetail : (token : RecoilState<any>, doctorId : string) => {
return client.get(`/manage/doctor/${doctorId}`, {
headers : {
......@@ -17,14 +24,21 @@ export default {
});
},
acceptDoctorRegReq : (token : RecoilState<any>, Data : any) => {
return client.post('/manage/doctor/accept', Data, {
return client.patch('/manage/doctor/accept', Data, {
headers : {
Authorization : token,
},
});
},
rejectDoctorRegReq : (token : RecoilState<any>, Data : any) => {
return client.post('/manage/doctor/reject', Data, {
return client.patch('/manage/doctor/reject', Data, {
headers : {
Authorization : token,
},
});
},
acceptDoctorSecReq : (token : RecoilState<any>, Data : any) => {
return client.patch('/manage/doctor/secession', Data, {
headers : {
Authorization : token,
},
......
import styled from 'styled-components';
export const Container = styled.div `
width : 100%;
position : relative;
margin : 5px 0;
padding : 20px 5px;
border-top : 1px solid #ddd;
display : flex;
flex-direction : column;
`;
export const TermsWrapper = styled.div `
width : 100%:
display : flex;
flex-direction : row;
padding : 0 0 20px 0;
margin : 0 0 10px 0;
background-color : transparent;
border : none;
border-bottom : 1px solid #ddd;
`;
export const EachTerms = styled.button `
color : #000;
background-color : transparent;
margin : 0 10px 0 0;
padding : 0 0 5px 0;
cursor : pointer;
font-size : 13px;
font-weight : 400;
border : none;
border-bottom : 1px solid;
transition : .25s all;
&:hover {
color : #337DFF;
opacity : .5;
}
`;
export const InfoWrapper = styled.div `
display : flex;
flex-direction : row;
border : none;
background-color : transparent;
`;
export const LicenseWrapper = styled.div `
flex : 1;
display : flex;
flex-direction : column;
border : none;
background-color : transparent;
justify-content : center;
align-items : flex-start;
`;
export const LicenseExplain = styled.div `
color : #a0a0a0;
font-size : 14px;
font-weight : 400;
padding : 0 5px;
border : none;
`;
export const LicenseImg = styled.img `
height : 60px;
width : 150px;
`;
export const ServiceInfoWrapper = styled.div `
flex : 3;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
border : none;
background-color : transparent;
`;
export const ServiceInfoEach = styled.div `
color : #d0d0d0;
font-size : 13px;
font-weight : 400
`;
\ No newline at end of file
import React from 'react';
import { RouteComponentProps } from 'react-router';
import * as Alert from '../../util/alertMessage';
import * as styled from './FooterStyled';
const ApiLicense = '/static/img/apiLicense.png';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface FooterProps extends RouteComponentProps {}
const Footer = (props : FooterProps) => {
const onGoTerm = () => {
Alert.onWarning('준비중입니다.', () => null);
};
const onGoPrivateLicense = () => {
Alert.onWarning('준비중입니다.', () => null);
};
const onGoServiceCenter = () => {
Alert.onWarning('준비중입니다.', () => null);
};
return (
<styled.Container>
<styled.TermsWrapper>
<styled.EachTerms
onClick = {onGoTerm}
>
이용약관
</styled.EachTerms>
<styled.EachTerms
onClick = {onGoPrivateLicense}
>
개인정보처리방침
</styled.EachTerms>
<styled.EachTerms
onClick = {onGoServiceCenter}
>
고객센터
</styled.EachTerms>
</styled.TermsWrapper>
<styled.InfoWrapper>
<styled.LicenseWrapper>
<styled.LicenseExplain>저작권</styled.LicenseExplain>
<styled.LicenseImg src = {ApiLicense}/>
</styled.LicenseWrapper>
<styled.ServiceInfoWrapper>
<styled.ServiceInfoEach>서비스명 : Smart Medicine Box (SMB)</styled.ServiceInfoEach>
<styled.ServiceInfoEach>서비스제공 : IoT 약병 제작회</styled.ServiceInfoEach>
<styled.ServiceInfoEach>담당자 : 박권수</styled.ServiceInfoEach>
<styled.ServiceInfoEach>주소 : OOO도 OOO시 OOO구 OOO OOO OOO</styled.ServiceInfoEach>
<styled.ServiceInfoEach>연락처 : 010 - 0000 - 0000</styled.ServiceInfoEach>
</styled.ServiceInfoWrapper>
<styled.LicenseWrapper/>
</styled.InfoWrapper>
</styled.Container>
)
};
export default Footer;
\ No newline at end of file
......@@ -10,7 +10,6 @@ import * as styled from './HeaderStyled';
import { authApi } from '../../api';
const headerImg = '/static/img/pharmacy.png';
const backButtonWhite = '/static/img/backButtonWhite.png';
const backButtonBlue = '/static/img/backButtonBlue.png';
const logout = '/static/img/logout.png';
......
import styled, { keyframes } from 'styled-components';
const ModalOn = keyframes `
0% {
background-color : rgba(52, 52, 52, .0);
}
20% {
background-color : rgba(52, 52, 52, .2);
}
40% {
background-color : rgba(52, 52, 52, .4);
}
60% {
background-color : rgba(52, 52, 52, .5);
}
80% {
background-color : rgba(52, 52, 52, .6);
}
100% {
background-color : rgba(52, 52, 52, .7);
}
`;
export const ModalContainer = styled.div `
height : 100%;
width : 100%;
z-index : 99;
position : absolute;
display : flex;
flex-direction : column;
animation : ${ModalOn} .5s;
background-color : rgba(52, 52, 52, .7);
`;
export const ModalClsButtonWrapper = styled.div `
flex : 1;
display : flex;
justify-content : flex-end;
align-items : center;
padding : 0 20px;
border : none;
background-color : transprent;
`;
export const ModalClsButton = styled.button `
border : none;
background-color : transparent;
cursor : pointer;
color : #fff;
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
transition : .25s all;
&:hover {
opacity : .5;
}
`;
export const ModalClsButtonImg = styled.img `
height : 20px;
width : 20px;
margin : 0 10px 0 0;
`;
export const ModalClsButtonText = styled.div `
font-size : 18px;
font-weight : 700;
`;
export const ModalContentWrapper = styled.div `
flex : 8;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
border : none;
`;
export const ModalContent = styled.div `
width : 600px;
height : 400px;
background-color : #fff;
border : 1.2px solid #337DFF;
border-radius : 5px;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
`;
\ No newline at end of file
import React from 'react';
import * as styled from './ModalStyled';
const closeButton = '/static/img/close.png';
interface ModalProps {
children : JSX.Element,
onModalClose : () => void;
}
const ModalContainer = (props : ModalProps) => {
return (
<styled.ModalContainer>
<styled.ModalClsButtonWrapper>
<styled.ModalClsButton
onClick = {props.onModalClose}
>
<styled.ModalClsButtonImg src = {closeButton}/>
<styled.ModalClsButtonText>닫기</styled.ModalClsButtonText>
</styled.ModalClsButton>
</styled.ModalClsButtonWrapper>
<styled.ModalContentWrapper>
<styled.ModalContent>
{props.children}
</styled.ModalContent>
</styled.ModalContentWrapper>
<styled.ModalClsButtonWrapper />
</styled.ModalContainer>
);
};
export default ModalContainer;
\ No newline at end of file
import moment from 'moment';
export const make = (chartData : any[], numberOfRow : number) => {
export const make = (takeMedicineHist : any[], numberOfRow : number) => {
const now = new Date();
const result : any = {};
new Array(numberOfRow).fill(null).forEach((item : any, index : number) => {
new Array(numberOfRow).fill(null).forEach(() => {
const key = moment(now).format('MM/DD');
result[key] = 0;
now.setDate(now.getDate() - 1);
})
});
chartData.forEach((data : any) => {
takeMedicineHist.forEach((data : any) => {
const key : string = moment(data.takeDate).format('MM/DD');
result[key] = result[key] + 1;
!isNaN(result[key]) ? result[key] = result[key] + data.dosage : null;
});
const categories : any = [];
......@@ -28,5 +28,5 @@ export const make = (chartData : any[], numberOfRow : number) => {
return {
categories,
data,
}
};
};
\ No newline at end of file
......
......@@ -9,6 +9,12 @@ export const token = atom({
effects_UNSTABLE : [persistAtom],
});
export const userId = atom({
key : 'userId',
default : null,
effects_UNSTABLE : [persistAtom],
});
export const userTypeCd = atom({
key : 'userTypeCd',
default : 'NORMAL',
......
......@@ -3,6 +3,7 @@ import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
import Error from '../components/error';
import Loading from '../components/Loading';
import Footer from '../components/Footer';
import { LoginContainer } from "./login";
import { RegisterContainer } from './register';
import { MainContainer } from "./main";
......
......@@ -9,6 +9,7 @@ import * as Alert from '../../util/alertMessage';
import moment from 'moment';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
import BottleInfoPresenter from './BottleInfoPresenter';
import { doctorApi } from '../../api';
......@@ -35,6 +36,7 @@ const BottleInfoContainer = (props : BottleInfoProps) => {
takeMedicineHist : [],
});
//차트에 표시되는 행의 개수
const numberOfChartItem = 7;
const [chartOption, setChartOption] = useState<any>({
chart : {
......@@ -51,19 +53,22 @@ const BottleInfoContainer = (props : BottleInfoProps) => {
categories : [],
},
series : [{
name : '약 복용 횟수',
name : '약 복용 회분',
color : '#337DFF',
data : [],
}],
});
const [takeMedicineHist, setTakeMedicineHist] = useState<any[]>([]);
const [feedback, setFeedback] = useState<string>('');
const [fdbType, setFdbType] = useState<string>('RECOMMEND');
const [medicineInfoModal, setMedicineInfoModal] = useState<boolean>(false);
const [modalType, setModalType] = useState<string>('hist'); //hist , info
const fetchData = async () => {
setModalType('hist');
setFeedback('');
setFdbType('RECOMMEND');
setMedicineInfoModal(false);
......@@ -71,6 +76,12 @@ const BottleInfoContainer = (props : BottleInfoProps) => {
try {
const result = await doctorApi.getPatientBottleDetail(token, bottleId);
if (result.statusText === 'OK') {
setTakeMedicineHist(result.data.takeMedicineHist.map((takeMedicine : any) => {
return ({
...takeMedicine,
takeDate : moment(takeMedicine.takeDate).format('YYYY년 MM월 DD일 hh시 mm분'),
});
}));
const { categories, data } = makeChart.make(result.data.takeMedicineHist, numberOfChartItem);
setBottleInfo({
...result.data,
......@@ -97,9 +108,8 @@ const BottleInfoContainer = (props : BottleInfoProps) => {
Alert.onError('접근 권한이 없습니다.', () => props.history.push('/'));
}
} catch(e : any) {
Alert.onError(e.response.data.error, () => props.history.push('/'));
console.log(e);
Alert.onError('알 수 없는 에러가 발생했습니다.', () => props.history.push('/'));
}
};
......@@ -123,7 +133,7 @@ const BottleInfoContainer = (props : BottleInfoProps) => {
Alert.onError('피드백 등록에 실패했습니다.', () => null);
}
} catch(e : any) {
Alert.onError(e.response.data.error, () => fetchData());
Alert.onError('알 수 없는 에러가 발생했습니다.', () => fetchData());
}
} else {
Alert.onError('피드백 내용을 입력하세요.', () => null);
......@@ -134,6 +144,14 @@ const BottleInfoContainer = (props : BottleInfoProps) => {
};
const onViewTakeHist = () => {
if(modalType === 'info') setModalType('hist');
};
const onViewMedicineInfo = () => {
if(modalType === 'hist') setModalType('info');
};
useEffect(() => {
if(userTypeCd !== 'DOCTOR') {
......@@ -148,8 +166,12 @@ const BottleInfoContainer = (props : BottleInfoProps) => {
<BottleInfoPresenter
bottleInfo = {bottleInfo}
chartOption = {chartOption}
takeMedicineHist = {takeMedicineHist}
medicineInfoModal = {medicineInfoModal}
modalType = {modalType}
onViewTakeHist = {onViewTakeHist}
onViewMedicineInfo = {onViewMedicineInfo}
setMedicineInfoModal = {setMedicineInfoModal}
feedback = {feedback}
......@@ -158,6 +180,7 @@ const BottleInfoContainer = (props : BottleInfoProps) => {
setFdbType = {setFdbType}
onSubmitFeedback = {onSubmitFeedback}
/>
<Footer {...props}/>
</>
);
};
......
......@@ -2,10 +2,10 @@ import React from 'react';
import HighCharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import Modal from '../../components/Modal';
import * as styled from './BottleInfoStyled';
const plus = '/static/img/plus.png';
const closeButton = '/static/img/close.png';
interface BottleInfoProps {
......@@ -16,8 +16,12 @@ interface BottleInfoProps {
takeMedicineHist : any[];
};
chartOption : any;
takeMedicineHist : any[];
medicineInfoModal : boolean;
modalType : string;
onViewTakeHist : () => void;
onViewMedicineInfo : () => void;
setMedicineInfoModal : (arg0 : boolean) => void;
feedback : string;
......@@ -32,17 +36,61 @@ const BottleInfoPresenter = (props : BottleInfoProps) => {
<styled.Container>
{
props.medicineInfoModal ?
<styled.ModalContainer>
<styled.ModalClsButtonWrapper>
<styled.ModalClsButton
onClick = {() => props.setMedicineInfoModal(false)}
<Modal onModalClose = {() => props.setMedicineInfoModal(false)}>
<>
<styled.ModalTypeButtonWrapper>
<styled.ModalTypeButton
isSelect = {props.modalType === 'hist'}
onClick = {props.onViewTakeHist}
>
복용 기록
</styled.ModalTypeButton>
<styled.ModalTypeButton
isSelect = {props.modalType === 'info'}
onClick = {props.onViewMedicineInfo}
>
약 정보
</styled.ModalTypeButton>
</styled.ModalTypeButtonWrapper>
{
props.modalType === 'hist' ?
<>
<styled.MedicineNameWrapper>
<styled.MedicineName>{`복용 기록`}</styled.MedicineName>
<styled.MedicineName style = {{color : '#343434', fontSize : 15, marginTop : 4,}}>{`전체 : ${props.takeMedicineHist.length}건`}</styled.MedicineName>
</styled.MedicineNameWrapper>
<styled.MedicineInfoWrapper>
{
props.takeMedicineHist.map((hist : any) => {
return (
<styled.HistWrapper
key = {hist._id}
>
<styled.ModalClsButtonImg src = {closeButton}/>
<styled.ModalClsButtonText>닫기</styled.ModalClsButtonText>
</styled.ModalClsButton>
</styled.ModalClsButtonWrapper>
<styled.ModalContentWrapper>
<styled.ModalContent>
<styled.HistDtmWrapper>
<styled.HistInfoEachWrapper style = {{fontSize : 11}}>복용 날짜</styled.HistInfoEachWrapper>
<styled.HistDtm>{hist.takeDate}</styled.HistDtm>
</styled.HistDtmWrapper>
<styled.HistInfoWrapper>
<styled.HistInfoEachWrapper>
복용량
<styled.HistInfoEach>{hist.dosage}회분</styled.HistInfoEach>
</styled.HistInfoEachWrapper>
<styled.HistInfoEachWrapper>
약병 내 온도
<styled.HistInfoEach>{hist.temperature}℃</styled.HistInfoEach>
</styled.HistInfoEachWrapper>
<styled.HistInfoEachWrapper>
약병 내 습도
<styled.HistInfoEach>{hist.humidity}%</styled.HistInfoEach>
</styled.HistInfoEachWrapper>
</styled.HistInfoWrapper>
</styled.HistWrapper>
)
})
}
</styled.MedicineInfoWrapper>
</> :
<>
<styled.MedicineNameWrapper>
<styled.MedicineName>{props.bottleInfo.medicine.name}</styled.MedicineName>
<styled.MedicineName style = {{color : '#343434', fontSize : 15, marginTop : 4,}}>{props.bottleInfo.medicine.company}</styled.MedicineName>
......@@ -65,9 +113,10 @@ const BottleInfoPresenter = (props : BottleInfoProps) => {
<styled.MedicineEachInfo style = {{color : '#9B0000'}}>{props.bottleInfo.medicine.antiEffect}</styled.MedicineEachInfo>
</styled.MedicineEachInfoWrapper>
</styled.MedicineInfoWrapper>
</styled.ModalContent>
</styled.ModalContentWrapper>
</styled.ModalContainer> : null
</>
}
</>
</Modal> : null
}
<styled.ChartAndFeedbackWrapper>
<styled.ChartWrapper>
......@@ -143,7 +192,6 @@ const BottleInfoPresenter = (props : BottleInfoProps) => {
<styled.NewFeedbackRegButton
onClick = {props.onSubmitFeedback}
>
{/* <styled.NewFeedbackRegButtonImg /> */}
<styled.NewFeedbackRegButtonText>피드백<br/>등록</styled.NewFeedbackRegButtonText>
</styled.NewFeedbackRegButton>
</styled.NewFeedbackButtonWrapper>
......
import styled, { keyframes } from 'styled-components';
const ModalOn = keyframes `
0% {
background-color : rgba(52, 52, 52, .0);
}
20% {
background-color : rgba(52, 52, 52, .2);
}
40% {
background-color : rgba(52, 52, 52, .4);
}
60% {
background-color : rgba(52, 52, 52, .5);
}
80% {
background-color : rgba(52, 52, 52, .6);
}
100% {
background-color : rgba(52, 52, 52, .7);
}
`;
import styled from 'styled-components';
export const Container = styled.div `
......@@ -32,91 +9,106 @@ export const Container = styled.div `
justify-content : center;
`;
export const ModalContainer = styled.div `
height : 100%;
width : 100%;
z-index : 99;
position : absolute;
export const ModalTypeButtonWrapper = styled.div `
border : none;
display : flex;
flex-direction : column;
animation : ${ModalOn} .5s;
background-color : rgba(52, 52, 52, .7);
flex-direction : row;
`;
width : 100%;
export const ModalClsButtonWrapper = styled.div `
flex : 1;
justify-content : center;
align-items : center;
display : flex;
background-color : transparent;
justify-content : flex-end;
align-items : center;
padding : 0 20px;
gap : 5%;
border : none;
background-color : transprent;
padding : 3% 0 0 0;
`;
export const ModalClsButton = styled.button `
border : none;
background-color : transparent;
cursor : pointer;
export const ModalTypeButton = styled.button<{isSelect : boolean}> `
border : 1px solid #337DFF;
border-radius : 3px;
color : ${props => props.isSelect ? '#fff' : '#337DFF'};
background-color : ${props => props.isSelect ? '#337DFF' : '#fff'};
color : #fff;
padding : 1% 3%;
display : flex;
flex-direction : row;
cursor : pointer;
justify-content : center;
align-items : center;
font-size : 16px;
font-weight : ${props => props.isSelect ? '600' : '500'};
transition : .25s all;
&:hover {
opacity : .5;
}
`;
export const ModalClsButtonImg = styled.img `
height : 20px;
width : 20px;
export const HistWrapper = styled.div `
display : flex;
flex-direction : column;
margin : 0 10px 0 0;
`;
height : 65px;
width : 100%;
export const ModalClsButtonText = styled.div `
font-size : 18px;
font-weight : 700;
border : none;
border-bottom : 1px solid #ddd;
`;
export const ModalContentWrapper = styled.div `
flex : 8;
export const HistDtmWrapper = styled.div `
flex : 2;
padding : 0 3%;
border : none;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
border : none;
`;
export const ModalContent = styled.div `
width : 700px;
height : 500px;
export const HistDtm = styled.div `
font-size : 16px;
color : #000;
`;
export const HistInfoWrapper = styled.div `
flex : 1;
padding : 0 3%;
background-color : #fff;
border : 1.2px solid #337DFF;
border-radius : 5px;
border : none;
display : flex;
flex-direction : column;
flex-direction : row;
align-items : flex-start;
`;
export const HistInfoEachWrapper = styled.div `
flex : 1;
border : none;
// justify-content : center;
display : flex;
flex-direction : row;
align-items : center;
gap : 5%;
font-size : 14px;
font-weight : 500;
color : #a0a0a0;
`;
export const HistInfoEach = styled.div `
font-size : 15px;
font-weight : 600;
color : #337DFF;
`;
export const MedicineNameWrapper = styled.div `
......@@ -169,6 +161,7 @@ export const MedicineEachInfoWrapper = styled.div `
display : flex;
flex-direction : column;
width : 80%;
padding : 20px 10%;
border : none;
......
......@@ -23,6 +23,7 @@ const LoginContainer = (props : LoginProps) => {
});
const [token, setToken] = useRecoilState(recoilUtil.token);
const [userId, setUserId] = useRecoilState(recoilUtil.userId);
const [userTypeCd, setUserTypeCd] = useRecoilState(recoilUtil.userTypeCd);
......@@ -58,13 +59,14 @@ const LoginContainer = (props : LoginProps) => {
const result : any = await authApi.login(loginForm);
if(result.statusText === 'OK' && result.data.userTypeCd !== 'NORMAL') {
setToken(result.data.token);
setUserId(loginForm.userId);
setUserTypeCd(result.data.userTypeCd);
Alert.onSuccess('로그인 성공, 메인 화면으로 이동합니다.', () => props.history.push('/'));
} else if(result.data.userTypeCd === 'NORMAL') {
Alert.onError('권한이 없는 유저입니다.', () => props.history.push('/'));
}
} catch(e) {
Alert.onError(e.response.data.error, () => null);
} catch(e : any) {
Alert.onError('알 수 없는 에러가 발생했습니다.', () => null);
}
};
......
......@@ -6,6 +6,7 @@ import * as recoilUtil from '../../util/recoilUtil';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
import DoctorMenuContainer from './doctor';
import ManagerMenuContainer from './manager';
......@@ -32,6 +33,7 @@ const MainContainer = (props : MainProps) => {
userTypeCd === 'MANAGER' ?
<ManagerMenuContainer {...props}/> : null
}
<Footer {...props}/>
</>
);
};
......
import React from 'react';
import * as styled from './MainStyled';
interface MainProps {
userTypeCd : string;
}
const MainPresenter = (props : MainProps) => {
return (
<styled.Container>
<styled.InfoAndSearchWrapper>
<styled.InfoWrapper>
<styled.InfoSquare>
</styled.InfoSquare>
<styled.NewPatientButton>새 환자 등록</styled.NewPatientButton>
</styled.InfoWrapper>
<styled.SearchAndDetailWrapper>
<styled.SearchBarWrapper>
<styled.SearchBar
placeholder = '환자 이름'
/>
<styled.SearchButton>
검색
</styled.SearchButton>
</styled.SearchBarWrapper>
<styled.SearchResultWrapper>
</styled.SearchResultWrapper>
</styled.SearchAndDetailWrapper>
</styled.InfoAndSearchWrapper>
<styled.BottleListWrapper>
bottleListWrapper
</styled.BottleListWrapper>
</styled.Container>
)
};
export default MainPresenter;
\ No newline at end of file
......@@ -11,13 +11,13 @@ import * as Alert from '../../../util/alertMessage';
import { doctorApi, medicineApi } from '../../../api';
//toDo : Generate QR Code By Medicine Id
type DoctorMenuProps = RouteComponentProps
const DoctorMenuContainer = (props : DoctorMenuProps) => {
const token = useRecoilValue(recoilUtil.token);
const userId = useRecoilValue(recoilUtil.userId);
const [loading, setLoading] = useRecoilState(recoilUtil.loading);
const [doctorInfo, setDoctorInfo] = useState<any>({
......@@ -33,7 +33,7 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
const [info, setInfo] = useState<any>({
infoType : 'DOCTOR',
userNm : '',
userAge : 0,
birth : '',
contact : '',
doctorType : '',
patientInfo : '',
......@@ -48,13 +48,18 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
const [editPatientInfo, setEditPatientInfo] = useState<string>('');
const [newPatientRegisterModal, setNewPatientRegisterModal] = useState<boolean>(false);
const [newPatientSearchId, setNewPatientSearchId] = useState<string>('');
const [newPatientSearchContact, setNewPatientSearchContact] = useState<string>('');
const [newPatientSearchResult, setNewPatientSearchResult] = useState<any | null>(null);
const [prescribeModal, setPrescribeModal] = useState<boolean>(false);
const [prescribeModalStep, setPrescribeModalStep] = useState<number>(1);
const [searchMedicineKeyword, setSearchMedicineKeyword] = useState<string>('');
const [medicineList, setMedicineList] = useState<any>([]);
const [prescribeMedicine, setPrescribeMedicine] = useState<any>(null);
const [dailyDosage, setDailyDosage] = useState<string>('1');
const [totalDay, setTotalDay] = useState<string>('1');
const [qrcodeUrl, setQrcodeUrl] = useState<string | null>(null);
const fetchData = async() => {
......@@ -69,7 +74,7 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
userNm : doctorInfo.doctorNm,
doctorType : doctorInfo.doctorType,
contact : doctorInfo.contact,
userAge : null,
birth : null,
patientInfo : '',
});
......@@ -79,7 +84,7 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
}).catch(error => console.log(error));
}
setLoading(false);
} catch(e) {
} catch(e : any) {
console.log(e);
setLoading(false);
}
......@@ -94,10 +99,12 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
setLoading(true);
await doctorApi.getPatientDetail(token, patientId).then(res => {
setPatientDetail(res.data);
const birth = res.data.profile.birth.split('-');
setInfo({
infoType : 'PATIENT',
userNm : res.data.profile.userNm,
userAge : res.data.profile.userAge,
birth : `${birth[0]}년 ${birth[1]}월 ${birth[2]}일`,
contact : res.data.profile.contact,
doctorType : null,
patientInfo : res.data.info,
......@@ -118,7 +125,7 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
userNm : doctorInfo.doctorNm,
doctorType : doctorInfo.doctorType,
contact : doctorInfo.contact,
userAge : null,
birth : null,
patientInfo : '',
});
setFilteringPatientList([]);
......@@ -145,7 +152,7 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
}
} catch(e : any) {
Alert.onError(e.response.data.error, () => null);
Alert.onError('알 수 없는 에러가 발생했습니다.', () => null);
}
};
......@@ -158,15 +165,15 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
};
const onSetNewPatientSearchId = (e : React.ChangeEvent<HTMLInputElement>) => {
setNewPatientSearchId(e.target.value);
const onSetNewPatientSearchContact = (e : React.ChangeEvent<HTMLInputElement>) => {
setNewPatientSearchContact(e.target.value);
};
const onSearchNewPatientByEmail = async () => {
const onSearchNewPatientByContact = async () => {
try {
setLoading(true);
await doctorApi.searchPatientById(token, newPatientSearchId).then(res => {
setNewPatientSearchResult(res.data);
await doctorApi.searchPatientByContact(token, newPatientSearchContact).then(res => {
setNewPatientSearchResult(res.data.patientInfo);
setLoading(false);
}).catch(err => {
console.log(err);
......@@ -176,29 +183,30 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
});
} catch(e : any) {
setLoading(false);
Alert.onError(e.response.data.error, () => null);
Alert.onError('알 수 없는 에러가 발생했습니다.', () => null);
}
};
const onRegisterNewPatient = () => {
if(newPatientSearchResult) {
const { patientId, patientNm } = newPatientSearchResult;
const { userId, userNm } = newPatientSearchResult;
const onRegisterReq = async () => {
try {
const result = await doctorApi.registerPatient(token, {
patientId,
patientId : userId,
});
if(result.statusText === 'OK') {
Alert.onSuccess('환자에게 담당의 등록 요청을 전송했습니다.', () => null);
setNewPatientRegisterModal(false);
} else {
Alert.onError('환자에게 담당의 등록 요청을 실패했습니다.', () => null);
}
} catch(e : any) {
Alert.onError(e.response.data.error, () => null);
Alert.onError('알 수 없는 에러가 발생했습니다.', () => null);
}
};
Alert.onCheck(`${patientNm} 환자에게 담당의 등록 요청을 전송하시겠습니까?`, onRegisterReq, () => null);
Alert.onCheck(`${userNm} 환자에게 담당의 등록 요청을 전송하시겠습니까?`, onRegisterReq, () => null);
} else {
Alert.onError('환자를 먼저 검색해주세요.', () => null);
}
......@@ -206,14 +214,17 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
const onCloseModal = async () => {
setNewPatientRegisterModal(false);
setNewPatientSearchId('');
setNewPatientSearchContact('');
setNewPatientSearchResult(null);
setEditModal(false);
setEditPatientInfo('');
setPrescribeModal(false);
setPrescribeModalStep(1);
setSearchMedicineKeyword('');
setMedicineList([]);
setPrescribeMedicine(null);
setDailyDosage('1');
setTotalDay('1');
};
const onGoBottleDetail = (bottleId : number) => {
......@@ -231,18 +242,68 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
setLoading(true);
const res = await medicineApi.searchMedicine(token, searchMedicineKeyword);
if(res.statusText === 'OK') {
console.log(res.data.medicineList)
setMedicineList(res.data.medicineList);
}
setLoading(false);
} catch(e : any) {
Alert.onError(e.response.data.error, () => null);
Alert.onError('알 수 없는 에러가 발생했습니다.', () => null);
}
};
const onSetDailyDosage = (e : React.ChangeEvent<HTMLInputElement>) => {
setDailyDosage(e.target.value);
};
const onSetTotalDay = (e : React.ChangeEvent<HTMLInputElement>) => {
setTotalDay(e.target.value);
};
const onSetNextStepPrescribe = () => {
if(prescribeMedicine) setPrescribeModalStep(prescribeModalStep + 1);
else Alert.onWarning('먼저 처방할 약을 선택해야 합니다.', () => null);
};
const onSetPrevStepPrescribe = () => {
if(prescribeModalStep > 1) setPrescribeModalStep(prescribeModalStep - 1);
};
const onPrescribeSubmit = async() => {
//toDo : 처방해서, QR코드 생성
Alert.onWarning('작업 중입니다.', () => null);
const onPrescribeMedicine = async () => {
setLoading(true);
try {
const res = await doctorApi.prescribeMedicine(token, {
patientId : patientDetail.profile.userId,
medicineId : prescribeMedicine.medicineId,
dailyDosage,
totalDosage : String(parseInt(totalDay) * parseInt(dailyDosage)),
});
if(res.statusText === 'OK') {
setQrcodeUrl(res.data.qrCode);
setLoading(false);
Alert.onSuccess('처방 정보가 생성 되었습니다.', () => onSetNextStepPrescribe());
}
} catch(e : any) {
setLoading(false);
Alert.onError('알 수 없는 에러가 발생했습니다.', () => null);
}
};
Alert.onCheck(`${prescribeMedicine.name}(일 복용량:${dailyDosage})\n을 ${totalDay}일동안 처방하시겠습니까?`, async () => {
await onPrescribeMedicine();
}, () => null);
};
const onPrintQrcode = async(divId : string) => {
const printContent : any = document.getElementById(divId);
const windowOpen : any = window.open('', '_blank');
//toDo : 현재 인증되지 않은 사용자(=http)이기 때문에, GCS에서 signed url을 불러와도 만료되어, 이미지가 정상 표시 안됨 : 해결 필요
windowOpen.document.writeln(printContent.innerHTML);
windowOpen.document.close();
windowOpen.focus();
windowOpen.print();
windowOpen.close();
};
const onPrescribeCancel = () => {
......@@ -289,21 +350,30 @@ const DoctorMenuContainer = (props : DoctorMenuProps) => {
newPatientRegisterModal = {newPatientRegisterModal}
setNewPatientRegisterModal = {setNewPatientRegisterModal}
newPatientSearchId = {newPatientSearchId}
onSetNewPatientSearchId = {onSetNewPatientSearchId}
onSearchNewPatientByEmail = {onSearchNewPatientByEmail}
newPatientSearchContact = {newPatientSearchContact}
onSetNewPatientSearchContact = {onSetNewPatientSearchContact}
onSearchNewPatientByContact = {onSearchNewPatientByContact}
onRegisterNewPatient = {onRegisterNewPatient}
onCloseModal = {onCloseModal}
prescribeModal = {prescribeModal}
prescribeModalStep = {prescribeModalStep}
onSetNextStepPrescribe = {onSetNextStepPrescribe}
onSetPrevStepPrescribe = {onSetPrevStepPrescribe}
setPrescribeModal = {setPrescribeModal}
searchMedicineKeyword = {searchMedicineKeyword}
onSetSearchMedicineKeyword = {onSetSearchMedicineKeyword}
medicineList = {medicineList}
searchMedicine = {searchMedicine}
prescribeMedicine = {prescribeMedicine}
dailyDosage = {dailyDosage}
onSetDailyDosage = {onSetDailyDosage}
totalDay = {totalDay}
onSetTotalDay = {onSetTotalDay}
qrcodeUrl = {qrcodeUrl}
setPrescribeMedicine = {setPrescribeMedicine}
onPrescribeSubmit = {onPrescribeSubmit}
onPrintQrcode = {onPrintQrcode}
onPrescribeCancel = {onPrescribeCancel}
newPatientSearchResult = {newPatientSearchResult}
......
import React from 'react';
import Modal from '../../../components/Modal';
import * as styled from './DoctorMenuStyled';
const medicineImg = '/static/img/medicine.png';
const addButton = '/static/img/plus.png';
const lensImg = '/static/img/lens.png';
const closeButton = '/static/img/close.png';
const edit = '/static/img/edit.png';
const refreshing = '/static/img/refreshing.png';
const check = '/static/img/check.png';
......@@ -18,7 +19,7 @@ interface DoctorMenuProps {
userNm : string;
doctorType : string | null;
contact : string;
userAge : number | null;
birth : number | null;
patientInfo : string;
};
searchPatientKeyword : string;
......@@ -38,9 +39,9 @@ interface DoctorMenuProps {
newPatientRegisterModal : boolean;
setNewPatientRegisterModal : any;
newPatientSearchId: string;
onSetNewPatientSearchId : React.ChangeEventHandler<HTMLInputElement>;
onSearchNewPatientByEmail : () => void;
newPatientSearchContact: string;
onSetNewPatientSearchContact : React.ChangeEventHandler<HTMLInputElement>;
onSearchNewPatientByContact : () => void;
onRegisterNewPatient : () => void;
onCloseModal : () => void;
......@@ -48,6 +49,10 @@ interface DoctorMenuProps {
prescribeModal : boolean;
setPrescribeModal : any;
prescribeModalStep : number;
onSetNextStepPrescribe : () => void;
onSetPrevStepPrescribe : () => void;
searchMedicineKeyword : string;
onSetSearchMedicineKeyword : React.ChangeEventHandler<HTMLInputElement>;
......@@ -57,7 +62,15 @@ interface DoctorMenuProps {
prescribeMedicine : any;
setPrescribeMedicine : (arg0 : any) => void;
dailyDosage : string;
onSetDailyDosage : React.ChangeEventHandler<HTMLInputElement>;
totalDay : string;
onSetTotalDay : React.ChangeEventHandler<HTMLInputElement>;
qrcodeUrl : string | null;
onPrescribeSubmit : () => void;
onPrintQrcode : (arg0 : string) => void;
onPrescribeCancel : () => void;
}
......@@ -66,26 +79,17 @@ const DoctorMenuPresenter = (props : DoctorMenuProps) => {
<styled.Container>
{
props.newPatientRegisterModal ?
<styled.ModalContainer>
<styled.ModalClsButtonWrapper>
<styled.ModalClsButton
onClick = {() => props.setNewPatientRegisterModal(false)}
>
<styled.ModalClsButtonImg src = {closeButton}/>
<styled.ModalClsButtonText>닫기</styled.ModalClsButtonText>
</styled.ModalClsButton>
</styled.ModalClsButtonWrapper>
<styled.ModalContentWrapper>
<styled.ModalContent>
<Modal onModalClose = {() => props.setNewPatientRegisterModal(false)}>
<>
<styled.NewPatientRegisterTitle>새 환자 등록</styled.NewPatientRegisterTitle>
<styled.NewPatientSearchWrapper>
<styled.NewPatientSearchInput
placeholder = '환자 이메일을 입력하세요.'
value = {props.newPatientSearchId}
onChange = {props.onSetNewPatientSearchId}
placeholder = '환자의 연락처를 입력하세요.'
value = {props.newPatientSearchContact}
onChange = {props.onSetNewPatientSearchContact}
/>
<styled.NewPatientSearchButton
onClick = {props.onSearchNewPatientByEmail}
onClick = {props.onSearchNewPatientByContact}
>
<styled.NewPatientSearchButtonImg src = {lensImg}/>
</styled.NewPatientSearchButton>
......@@ -95,10 +99,18 @@ const DoctorMenuPresenter = (props : DoctorMenuProps) => {
props.newPatientSearchResult ?
<styled.NewPatientSearchResult>
<styled.NewPatientSearchResultInfoWrapper>
<styled.NewPatientSearchResultInfo>이름 : </styled.NewPatientSearchResultInfo>
<styled.NewPatientSearchResultInfo>
이름 :
<styled.NewPatientSearchResultInfoText>
{props.newPatientSearchResult.userNm}
</styled.NewPatientSearchResultInfoText>
</styled.NewPatientSearchResultInfo>
<styled.NewPatientSearchResultInfo>
생년월일 :
<styled.NewPatientSearchResultInfoText>
{props.newPatientSearchResult.patientNm}
{props.newPatientSearchResult.birth}
</styled.NewPatientSearchResultInfoText>
</styled.NewPatientSearchResultInfo>
</styled.NewPatientSearchResultInfoWrapper>
</styled.NewPatientSearchResult> :
'🤔검색 결과가 없습니다.'
......@@ -116,24 +128,13 @@ const DoctorMenuPresenter = (props : DoctorMenuProps) => {
취소
</styled.NewPatientRegisterButton>
</styled.NewPatientRegisterButtonWrapper>
</styled.ModalContent>
</styled.ModalContentWrapper>
<styled.ModalClsButtonWrapper/>
</styled.ModalContainer> : null
</>
</Modal> : null
}
{
props.editModal ?
<styled.ModalContainer>
<styled.ModalClsButtonWrapper>
<styled.ModalClsButton
onClick = {() => props.setEditModal(false)}
>
<styled.ModalClsButtonImg src = {closeButton}/>
<styled.ModalClsButtonText>닫기</styled.ModalClsButtonText>
</styled.ModalClsButton>
</styled.ModalClsButtonWrapper>
<styled.ModalContentWrapper>
<styled.ModalContent>
<Modal onModalClose = {() => props.setEditModal(false)}>
<>
<styled.PatientInfoViewContainer>
<styled.PatientInfoPatientNmWrapper>
<styled.PatientInfoPatientNmInfo>이름 : </styled.PatientInfoPatientNmInfo>
......@@ -170,27 +171,25 @@ const DoctorMenuPresenter = (props : DoctorMenuProps) => {
취소
</styled.PatientInfoEditButton>
</styled.PatientInfoEditButtonWrapper>
</styled.ModalContent>
</styled.ModalContentWrapper>
<styled.ModalClsButtonWrapper/>
</styled.ModalContainer> : null
</>
</Modal> : null
}
{
props.prescribeModal ?
<styled.ModalContainer>
<styled.ModalClsButtonWrapper>
<styled.ModalClsButton
onClick = {props.onCloseModal}
>
<styled.ModalClsButtonImg src = {closeButton}/>
<styled.ModalClsButtonText>닫기</styled.ModalClsButtonText>
</styled.ModalClsButton>
</styled.ModalClsButtonWrapper>
<styled.ModalContentWrapper>
<styled.ModalContent>
<Modal onModalClose = {props.onCloseModal}>
<>
<styled.MedicineSearchTitle>
약 검색
{
props.prescribeModalStep === 1 ?
'약 검색' :
props.prescribeModalStep === 2 ?
'복용량 입력' :
'처방 정보 QR코드'
}
</styled.MedicineSearchTitle>
{
props.prescribeModalStep === 1 ?
<>
<styled.MedicineSearchInputWrapper>
<styled.MedicineSearchInput
placeholder = '증상, 또는 약 이름을 검색하세요.'
......@@ -210,7 +209,10 @@ const DoctorMenuPresenter = (props : DoctorMenuProps) => {
return (
<styled.MedicineSearchResultEach
key = {medicine.medicineId}
onClick = {() => props.setPrescribeMedicine(medicine)}
onClick = {() => props.setPrescribeMedicine(
props.prescribeMedicine && props.prescribeMedicine.medicineId === medicine.medicineId ?
null : medicine
)}
>
<styled.MedicineSearchResultEachInfo>
{medicine.name}
......@@ -229,13 +231,65 @@ const DoctorMenuPresenter = (props : DoctorMenuProps) => {
</styled.NothingWrapper>
}
</styled.MedicineSearchResultWrapper>
</>
:
props.prescribeModalStep === 2 ?
<styled.MedicineDosageSetWrapper>
<styled.MedicineDosageInfo>
*하루 복용량을 입력하세요.
</styled.MedicineDosageInfo>
<styled.MedicineDosageInput
value = {props.dailyDosage}
onChange = {props.onSetDailyDosage}
min = {1}
max = {3}
/>
<styled.MedicineDosageInfo>
*총 며칠 분량인지 입력하세요.
</styled.MedicineDosageInfo>
<styled.MedicineDosageInput
value = {props.totalDay}
onChange = {props.onSetTotalDay}
/>
</styled.MedicineDosageSetWrapper>
:
<styled.MedicineQRCodeWrapper
id = 'qrCodePrint'
>
<styled.MedicineQRCodeInfo>
*어플리케이션에서 QR코드를 스캔하면 약병에 약이 등록됩니다.
</styled.MedicineQRCodeInfo>
{
props.qrcodeUrl ?
<styled.MedicineQRCode
src = {props.qrcodeUrl}/> : null
}
</styled.MedicineQRCodeWrapper>
}
<styled.MedicinePrescribeButtonWrapper>
{
props.prescribeModalStep === 1 ?
<styled.MedicinePrescribeButton
isClose = {false}
onClick = {props.onSetNextStepPrescribe}
>
다음 단계
</styled.MedicinePrescribeButton> :
props.prescribeModalStep === 2 ?
<styled.MedicinePrescribeButton
isClose = {false}
onClick = {props.onPrescribeSubmit}
>
처방
</styled.MedicinePrescribeButton>
:
<styled.MedicinePrescribeButton
isClose = {false}
onClick = {() => props.onPrintQrcode('qrCodePrint')}
>
출력
</styled.MedicinePrescribeButton>
}
<styled.MedicinePrescribeButton
isClose = {true}
onClick = {props.onPrescribeCancel}
......@@ -243,10 +297,8 @@ const DoctorMenuPresenter = (props : DoctorMenuProps) => {
취소
</styled.MedicinePrescribeButton>
</styled.MedicinePrescribeButtonWrapper>
</styled.ModalContent>
</styled.ModalContentWrapper>
<styled.ModalClsButtonWrapper/>
</styled.ModalContainer> : null
</>
</Modal> : null
}
<styled.InfoAndSearchWrapper>
<styled.InfoWrapper>
......@@ -287,7 +339,7 @@ const DoctorMenuPresenter = (props : DoctorMenuProps) => {
</styled.InfoEachWrapper>
<styled.InfoEachWrapper>
<styled.InfoEachTopic>생년월일</styled.InfoEachTopic>
<styled.InfoEachText>{props.info.userAge}세</styled.InfoEachText>
<styled.InfoEachText>{props.info.birth}</styled.InfoEachText>
</styled.InfoEachWrapper>
<styled.InfoEachWrapper>
<styled.InfoEachTopic>연락처</styled.InfoEachTopic>
......@@ -351,7 +403,7 @@ const DoctorMenuPresenter = (props : DoctorMenuProps) => {
onClick = {() => props.onFetchPatientDetail(patient.userId)}
>
<styled.SearchResultEachText isLast = {false}>{patient.userNm}</styled.SearchResultEachText>
<styled.SearchResultEachText isLast = {false}>{patient.userAge}세</styled.SearchResultEachText>
<styled.SearchResultEachText isLast = {false}>{patient.birth}</styled.SearchResultEachText>
<styled.SearchResultEachText isLast = {true}>{patient.contact}</styled.SearchResultEachText>
</styled.SearchResultEach>
)
......
import styled, { keyframes } from 'styled-components';
const ModalOn = keyframes `
0% {
background-color : rgba(52, 52, 52, .0);
}
20% {
background-color : rgba(52, 52, 52, .2);
}
40% {
background-color : rgba(52, 52, 52, .4);
}
60% {
background-color : rgba(52, 52, 52, .5);
}
80% {
background-color : rgba(52, 52, 52, .6);
}
100% {
background-color : rgba(52, 52, 52, .7);
}
`;
import styled from 'styled-components';
export const Container = styled.div `
......@@ -32,93 +9,6 @@ export const Container = styled.div `
justify-content : center;
`;
export const ModalContainer = styled.div `
height : 100%;
width : 100%;
z-index : 99;
position : absolute;
display : flex;
flex-direction : column;
animation : ${ModalOn} .5s;
background-color : rgba(52, 52, 52, .7);
`;
export const ModalClsButtonWrapper = styled.div `
flex : 1;
display : flex;
justify-content : flex-end;
align-items : center;
padding : 0 20px;
border : none;
background-color : transprent;
`;
export const ModalClsButton = styled.button `
border : none;
background-color : transparent;
cursor : pointer;
color : #fff;
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
transition : .25s all;
&:hover {
opacity : .5;
}
`;
export const ModalClsButtonImg = styled.img `
height : 20px;
width : 20px;
margin : 0 10px 0 0;
`;
export const ModalClsButtonText = styled.div `
font-size : 18px;
font-weight : 700;
`;
export const ModalContentWrapper = styled.div `
flex : 8;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
border : none;
`;
export const ModalContent = styled.div `
width : 600px;
height : 400px;
background-color : #fff;
border : 1.2px solid #337DFF;
border-radius : 5px;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
`;
export const NewPatientRegisterTitle = styled.div `
font-size : 20px;
font-weight : 700;
......@@ -199,14 +89,18 @@ export const NewPatientSearchResult = styled.div `
export const NewPatientSearchResultInfoWrapper = styled.div `
display : flex;
flex-direction : column;
`;
export const NewPatientSearchResultInfo = styled.div `
display : flex;
flex-direction : row;
font-size : 13px;
font-weight : 600;
color : #a0a0a0;
margin : 0 5px 0 0;
margin : 0 5px 0 5px;
`;
export const NewPatientSearchResultInfoText = styled.div `
......@@ -214,6 +108,8 @@ export const NewPatientSearchResultInfoText = styled.div `
color : #343434;
font-weight : 600;
letter-spacing : 1px;
margin : 0 0 0 5px;
`;
export const NewPatientRegisterButtonWrapper = styled.div `
......@@ -436,6 +332,7 @@ export const MedicineSearchButtonImg = styled.img `
height : 15px;
width : 15px;
transition : .25s all;
`;
export const MedicineSearchResultWrapper = styled.div `
......@@ -499,6 +396,91 @@ export const MedicineSelectButtonImg = styled.img `
width : 15px;
`;
export const MedicineDosageSetWrapper = styled.div `
width : 80%;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
border : none;
margin : 20px 0;
height : 200px;
`;
export const MedicineDosageInfo = styled.div `
font-size : 15px;
font-weight : 500;
color : #a0a0a0;
width : 100%;
margin : 10px 0 10px 0;
border : none;
background-color : transparent;
text-align : center;
`;
export const MedicineDosageInput = styled.input.attrs({
type : 'number',
}) `
margin : 0 0 10px 0;
width : 40%;
padding : 10px 20px;
color : #337DFF;
font-size : 20px;
font-weight : 700;
border : none;
border-bottom : 1px solid #337DFF;
display : flex;
flex-direction : row;
text-align : center;
transition : .25s all;
`;
export const MedicineQRCodeWrapper = styled.div `
width : 80%;
height : 200px;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
margin : 20px 0;
border : none;
`;
export const MedicineQRCodeInfo = styled.div `
font-size : 15px;
font-weight : 500;
color : #a0a0a0;
text-align : center;
`;
export const MedicineQRCode = styled.img `
margin : 10px 0 0 0;
height : 170px;
width : 170px;
`;
export const MedicinePrescribeButtonWrapper = styled.div `
margin : 20px 0 0 0;
......
......@@ -17,11 +17,14 @@ const ManagerMenuContainer = (props : ManagerMenuProps) => {
const token = useRecoilValue(recoilUtil.token);
const [doctorRegReqList, setDoctorRegReqList] = useState<any>([]);
const [doctorList, setDoctorList] = useState<any>([]);
const [viewType, setViewType] = useState<string>('reg');
const [doctorDetail, setDoctorDetail] = useState<any>({});
const [modalUp, setModalUp] = useState<boolean>(false);
const [validate, setValidate] = useState<string>('W');
const [validateDoctorLicense, setValidateDoctorLicense] = useState<string>('');
......@@ -30,20 +33,29 @@ const ManagerMenuContainer = (props : ManagerMenuProps) => {
setValidate('W');
try {
await managerApi.getDoctorRegReqList(token)
.then((res : any) => {
const res = viewType === 'reg' ? await managerApi.getDoctorRegReqList(token) : await managerApi.getDoctorSecReqList(token);
if(res.statusText === 'OK') {
setDoctorRegReqList(res.data.doctorRegReqList);
setDoctorList(res.data.doctorList);
} else {
Alert.onError(res.data.error, () => null);
}
}).catch(err => {
Alert.onError(err.response.data.error, () => null);
})
} catch(e) {
Alert.onError(e.response.data.error, () => null);
} catch(e : any) {
console.log(e);
Alert.onError('알 수 없는 에러가 발생했습니다.', () => null);
}
};
//가입요청 보기
const onViewRegList = () => {
setViewType('reg');
};
//탈퇴요청 보기
const onViewSecList = () => {
setViewType('sec');
};
//가입요청 상세보기
const onViewDetailReq = async (doctorId : string) => {
setValidate('W');
......@@ -55,13 +67,23 @@ const ManagerMenuContainer = (props : ManagerMenuProps) => {
setModalUp(true);
}
})
} catch(e) {
Alert.onError(e.response.data.error, () => setModalUp(false));
} catch(e : any) {
Alert.onError('알 수 없는 에러가 발생했습니다.', () => setModalUp(false));
}
};
const onViewLicenseDetail = async (url : string) => {
const licensePage : any = window.open(url);
licensePage.focus();
};
//자격 확인 문서를 보고, 면허 번호를 입력하는 함수
const onSetValidateDoctorLicense = (e : React.ChangeEvent<HTMLInputElement>) => {
setValidateDoctorLicense(e.target.value);
};
//회원 가입 수락
const onAcceptRequest = () => {
const onAcceptRegReq = () => {
if(validate === 'W') {
Alert.onError('먼저 의사의 자격번호가 유효한지 검증해주세요.', () => null);
return;
......@@ -72,14 +94,16 @@ const ManagerMenuContainer = (props : ManagerMenuProps) => {
const onAccept = async() => {
try {
await managerApi.acceptDoctorRegReq(token, doctorDetail)
.then((res : any) => {
await managerApi.acceptDoctorRegReq(token, {
doctorId : doctorDetail.doctorId,
validateDoctorLicense,
}).then((res : any) => {
if(res.statusText === 'OK') {
Alert.onSuccess('회원 등록이 완료되었습니다.', fetchData);
}
})
} catch(e) {
Alert.onError(e.response.data.error, () => setModalUp(false));
});
} catch(e : any) {
Alert.onError('알 수 없는 에러가 발생했습니다.', () => setModalUp(false));
}
};
......@@ -90,58 +114,95 @@ const ManagerMenuContainer = (props : ManagerMenuProps) => {
const onRejectRequest = () => {
const onReject = async() => {
try {
await managerApi.rejectDoctorRegReq(token, doctorDetail)
.then((res : any) => {
await managerApi.rejectDoctorRegReq(token, {
doctorId : doctorDetail.doctorId,
}).then((res : any) => {
if(res.statusText === 'OK') {
Alert.onSuccess('회원 등록이 취소되었습니다.', fetchData);
}
})
} catch(e) {
Alert.onError(e.response.data.error, () => setModalUp(false));
});
} catch(e : any) {
Alert.onError('알 수 없는 에러가 발생했습니다.', () => setModalUp(false));
}
};
Alert.onCheck('회원 가입 요청을 취소하시겠습니까?', onReject, () => null);
};
//회원 자격번호 유효 검증 api
const onValidate = async () => {
try {
await managerApi.validateDoctorLicense(token, {
doctorLicense : doctorDetail.info.doctorLicense,
validateDoctorLicense,
}).then(res => {
if(res.statusText === 'OK') {
setValidate(res.data.result ? 'Y' : 'N');
}
}).catch(err => {
Alert.onError(err.response.data, () => {
console.log(err);
Alert.onError('알 수 없는 에러가 발생했습니다.', () => {
setModalUp(false);
setValidate('W');
});
})
} catch(e) {
Alert.onError(e.response.data, () => {
} catch(e : any) {
Alert.onError('알 수 없는 에러가 발생했습니다.', () => {
setModalUp(false);
setValidate('W');
});
}
};
const onAcceptSecReq = (doctorId : string) => {
const onAccept = async () => {
try {
const res = await managerApi.acceptDoctorSecReq(token, {
doctorId,
});
if(res.statusText === 'OK') {
Alert.onSuccess('탈퇴를 승인했습니다.', fetchData);
}
} catch (e : any) {
Alert.onError('알 수 없는 에러가 발생했습니다.', () => null);
}
};
Alert.onCheck('회원 탈퇴를 승인하시겠습니까?\n이 작업은 되돌릴 수 없습니다.', onAccept, () => null);
};
useEffect(() => {
setValidate('W');
setValidateDoctorLicense('');
}, [modalUp]);
useEffect(() => {
fetchData();
}, []);
}, [viewType]);
return (
<ManagerMenuPresenter
doctorRegReqList = {doctorRegReqList}
viewType = {viewType}
onViewRegList = {onViewRegList}
onViewSecList = {onViewSecList}
doctorList = {doctorList}
doctorDetail = {doctorDetail}
modalUp = {modalUp}
setModalUp = {setModalUp}
onViewDetailReq = {onViewDetailReq}
onViewLicenseDetail = {onViewLicenseDetail}
validate = {validate}
onValidate = {onValidate}
onAcceptRequest = {onAcceptRequest}
validateDoctorLicense = {validateDoctorLicense}
onSetValidateDoctorLicense = {onSetValidateDoctorLicense}
onAcceptRegReq = {onAcceptRegReq}
onAcceptSecReq = {onAcceptSecReq}
onRejectRequest = {onRejectRequest}
/>
);
......
import React from 'react';
import Modal from '../../../components/Modal';
import * as styled from './ManagerMenuStyled';
const closeButton = '/static/img/close.png';
interface ManagerMenuProps {
doctorRegReqList : any[];
viewType : string;
onViewRegList : () => void;
onViewSecList : () => void;
doctorList : any[];
doctorDetail : any;
modalUp : boolean;
setModalUp : any;
onViewDetailReq : (arg0 : string) => void;
onViewLicenseDetail : (arg0 : string) => void;
validate : string;
onValidate : () => void;
onAcceptRequest : () => void;
validateDoctorLicense : string;
onSetValidateDoctorLicense : React.ChangeEventHandler<HTMLInputElement>;
onAcceptRegReq : () => void;
onAcceptSecReq : (arg0 : string) => void;
onRejectRequest : () => void;
}
......@@ -26,26 +34,33 @@ const ManagerMenuPresenter = (props : ManagerMenuProps) => {
<styled.Container>
{
props.modalUp ?
<styled.ModalContainer>
<styled.ModalClsButtonWrapper>
<styled.ModalClsButton
onClick = {() => props.setModalUp(false)}
>
<styled.ModalClsButtonImg src = {closeButton}/>
<styled.ModalClsButtonText>닫기</styled.ModalClsButtonText>
</styled.ModalClsButton>
</styled.ModalClsButtonWrapper>
<styled.ModalContentWrapper>
<styled.ModalContent>
<Modal onModalClose = {() => props.setModalUp(false)}>
<>
<styled.ModalTitleWrapper>
<styled.ModalTitle>가입 요청 정보</styled.ModalTitle>
</styled.ModalTitleWrapper>
<styled.ModalBodyWrapper>
<styled.ModalBodyLeftAndRight>
<styled.ModalInfoWrapper>
<styled.ModalInfoExplain>의사 자격 번호</styled.ModalInfoExplain>
<styled.DoctorLicenseViewWrapper>
<styled.ModalInfoExplain>
의사 자격 번호
</styled.ModalInfoExplain>
<styled.DoctorLicenseViewButton onClick = {() => props.onViewLicenseDetail(props.doctorDetail.info.doctorLicense)}>
자격정보 확인
</styled.DoctorLicenseViewButton>
</styled.DoctorLicenseViewWrapper>
<styled.ModalInfoNotice>
* 자격 정보 확인 버튼을 눌러 정보를 확인하세요.
<br/>* 정보 확인은 15분간 유효합니다.
<br/>* 확인한 면허 번호를 입력 후 검증하세요.
</styled.ModalInfoNotice>
<styled.ModalInfo>
{props.doctorDetail.info.doctorLicense}
<styled.DoctorLicenseViewInput
placeholder = '의사 면허 번호'
value = {props.validateDoctorLicense}
onChange = {props.onSetValidateDoctorLicense}
/>
<styled.ValidateButton
onClick = {props.onValidate}
disabled = {props.validate !== 'W'}
......@@ -88,7 +103,7 @@ const ManagerMenuPresenter = (props : ManagerMenuProps) => {
</styled.ModalBodyWrapper>
<styled.ModalButtonWrapper>
<styled.ModalButton
onClick = {props.onAcceptRequest}
onClick = {props.onAcceptRegReq}
isAccept = {true}
>
수락
......@@ -100,19 +115,41 @@ const ManagerMenuPresenter = (props : ManagerMenuProps) => {
거절
</styled.ModalButton>
</styled.ModalButtonWrapper>
</styled.ModalContent>
</styled.ModalContentWrapper>
<styled.ModalClsButtonWrapper/>
</styled.ModalContainer> : null
</>
</Modal> : null
}
<styled.ContentWrapper>
<styled.ContentButtonWrapper>
<styled.ContentButton
isSelect = {props.viewType === 'reg'}
onClick = {props.onViewRegList}
>
가입 대기
</styled.ContentButton>
<styled.ContentButton
isSelect = {props.viewType === 'sec'}
onClick = {props.onViewSecList}
>
탈퇴 요청
</styled.ContentButton>
</styled.ContentButtonWrapper>
<styled.ContentTitle>
{
props.viewType === 'sec' ?
<>
탈퇴 대기 중 의사 회원
<styled.ContentExplain>
*승인을 누르면 탈퇴를 승인합니다.
</styled.ContentExplain>
</> :
<>
가입 대기 중 의사 회원
<styled.ContentExplain>
*클릭하면 상세정보를 확인할 수 있습니다.
</styled.ContentExplain>
</>
}
</styled.ContentTitle>
<styled.ContentBody>
<styled.ContentInfoWrapper>
<styled.ContentInfo
isLast = {false}
......@@ -125,18 +162,28 @@ const ManagerMenuPresenter = (props : ManagerMenuProps) => {
이름
</styled.ContentInfo>
<styled.ContentInfo
isLast = {true}
isLast = {props.viewType !== 'sec'}
>
이메일
</styled.ContentInfo>
{
props.viewType === 'sec' ?
<styled.ContentInfo
isLast = {true}
>
탈퇴 수락
</styled.ContentInfo> : null
}
</styled.ContentInfoWrapper>
<styled.ContentBody>
{
props.doctorRegReqList.length ?
props.doctorRegReqList.map((doctor : any) => {
props.doctorList.length ?
props.doctorList.map((doctor : any) => {
return (
<styled.EachContentWrapper
key = {doctor.doctorId}
onClick = {() => props.onViewDetailReq(doctor.doctorId)}
disabled = {props.viewType === 'sec'}
>
<styled.EachContentNm
isLast = {false}
......@@ -149,10 +196,22 @@ const ManagerMenuPresenter = (props : ManagerMenuProps) => {
{doctor.info.doctorNm}
</styled.EachContentNm>
<styled.EachContentNm
isLast = {true}
isLast = {props.viewType !== 'sec'}
>
{doctor.doctorId}
</styled.EachContentNm>
{
props.viewType === 'sec' ?
<styled.EachContentNm
isLast = {true}
>
<styled.AcceptButton
onClick = {() => props.onAcceptSecReq(doctor.doctorId)}
>
승인
</styled.AcceptButton>
</styled.EachContentNm> : null
}
</styled.EachContentWrapper>
)
}) :
......
import styled, { keyframes } from 'styled-components';
import styled from 'styled-components';
const ModalOn = keyframes `
0% {
background-color : rgba(52, 52, 52, .0);
}
20% {
background-color : rgba(52, 52, 52, .2);
}
40% {
background-color : rgba(52, 52, 52, .4);
}
60% {
background-color : rgba(52, 52, 52, .5);
}
80% {
background-color : rgba(52, 52, 52, .6);
}
100% {
background-color : rgba(52, 52, 52, .7);
}
`;
export const Container = styled.div `
width : 100%;
height : 80vh;
......@@ -33,92 +11,6 @@ export const Container = styled.div `
`;
export const ModalContainer = styled.div `
height : 100%;
width : 100%;
z-index : 99;
position : absolute;
display : flex;
flex-direction : column;
animation : ${ModalOn} .5s;
background-color : rgba(52, 52, 52, .7);
`;
export const ModalClsButtonWrapper = styled.div `
flex : 1;
display : flex;
justify-content : flex-end;
align-items : center;
padding : 0 20px;
border : none;
background-color : transprent;
`;
export const ModalClsButton = styled.button `
border : none;
background-color : transparent;
cursor : pointer;
color : #fff;
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
transition : .25s all;
&:hover {
opacity : .5;
}
`;
export const ModalClsButtonImg = styled.img `
height : 20px;
width : 20px;
margin : 0 10px 0 0;
`;
export const ModalClsButtonText = styled.div `
font-size : 18px;
font-weight : 700;
`;
export const ModalContentWrapper = styled.div `
flex : 8;
display : flex;
flex-direction : column;
justify-content : center;
align-items : center;
border : none;
`;
export const ModalContent = styled.div `
width : 700px;
height : 500px;
background-color : #fff;
border : 1.2px solid #337DFF;
border-radius : 5px;
display : flex;
flex-direction : column;
// justify-content : center;
align-items : center;
`;
export const ModalTitleWrapper = styled.div `
flex : 1;
border : none;
......@@ -183,25 +75,96 @@ export const ModalInfoExplain = styled.div `
letter-spacing : 1px;
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
border : none;
border-bottom : 1px solid #337DFF;
color : #337DFF;
padding : 2px 5px;
padding : 2px 1px;
`;
export const ModalInfo = styled.div `
margin : 5px 0 20px 0;
font-size : 20px;
font-weight : 700;
margin : 5px 0 10px 0;
font-size : 13px;
font-weight : 600;
letter-spacing : 1px;
display : flex;
flex-direction : row;
align-items : center;
`;
export const ModalInfoNotice = styled.div `
font-size : 11px;
color : #bbb;
font-weight : 400;
letter-spacing : 0px;
margin : 5px 0 0px 0;
`;
export const DoctorLicenseViewWrapper = styled.div `
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
border : none;
background-color : transparent;
`;
export const DoctorLicenseViewButton = styled.button `
margin : 5px 0 0 7px;
border : 1px solid #343434;
border-radius : 3px;
background-color : #EAF2FF;
padding : 2px 5px;
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
cursor : pointer;
transition : .25s all;
&:hover {
border : 1px solid #337DFF;
background-color : #337DFF;
color : #fff;
}
font-size : 11px;
`;
export const DoctorLicenseViewInput = styled.input `
padding : 2px 1px;
font-size : 11px;
letter-spacing : 1px;
color : #343434;
border : none;
border-bottom : 1px solid #343434;
&::placeholder {
color : #ccc;
}
`;
export const ValidateButton = styled.button<{validate : string}> `
margin : 0 0 0 15px;
font-size : 11px;
margin : 0 0 0 5px;
padding : 2px 5px;
border-radius : 3px;
......@@ -288,8 +251,45 @@ export const ContentWrapper = styled.div `
`;
export const ContentButtonWrapper = styled.div `
width : 100%;
height : 10%;
border : none;
display : flex;
flex-direction : row;
justify-content : center;
align-items : flex-end;
gap : 10%;
background-color : transparent;
`;
export const ContentButton = styled.button<{isSelect : boolean}> `
background-color : ${props => props.isSelect ? '#337DFF' : 'transparent'};
color : ${props => props.isSelect ? '#fff' : '#337DFF'};
border : 1px solid #337DFF;
border-radius : 4px;
padding : 4px 10px;
cursor : pointer;
display : flex;
justify-content : center;
align-items : center;
transition : .25s all;
&:hover {
opacity : .5;
}
`;
export const ContentTitle = styled.div `
width : 100%;
height : 20%;
border : none;
border-bottom : 1px solid #ddd;
......@@ -298,7 +298,6 @@ export const ContentTitle = styled.div `
justify-content : center;
align-items : center;
padding : 4% 0;
font-size : 22px;
font-weight : 600;
letter-spacing : 1px;
......@@ -318,16 +317,17 @@ export const ContentExplain = styled.div `
export const ContentBody = styled.div `
overflow : scroll;
height : 79%;
min-height : 60%;
max-height : 60%;
border : none;
padding : 0 0 0 3px;
display : flex;
flex-direction : column;
align-items : center;
padding : 0 0 0 3px;
&::-webkit-scrollbar {
width : 3px;
background-color : transparent;
......@@ -341,6 +341,7 @@ export const ContentBody = styled.div `
export const ContentInfoWrapper = styled.div `
width : 100%;
height : 10%;
border : none;
border-bottom : 1px solid #a0a0a0;
......@@ -350,7 +351,6 @@ export const ContentInfoWrapper = styled.div `
justify-content : center;
align-items : center;
padding : 12px 0px;
`;
export const ContentInfo = styled.div<{isLast : boolean}> `
......@@ -388,6 +388,7 @@ export const EachContentWrapper = styled.button `
padding : 10px 0px;
:not(:disabled) {
cursor : pointer;
transition : .1s all;
......@@ -396,6 +397,7 @@ export const EachContentWrapper = styled.button `
background-color : #337DFF;
color : #fff;
}
}
`;
......@@ -417,10 +419,33 @@ export const EachContentNm = styled.div<{isLast : boolean}> `
`;
export const AcceptButton = styled.button `
background-color : transparent;
color : #337DFF;
border : 1px solid #337DFF;
border-radius : 3px;
padding : 2px 10px;
display : flex;
justify-content : center;
align-items : center;
cursor : pointer;
transition : .25s all;
&:hover {
background-color : #337DFF;
color : #fff;
}
`;
export const NothingWrapper = styled.div `
height : 100%;
width : 100%;
border : none;
display : flex;
justify-content : center;
align-items : center;
......
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { RouteComponentProps } from 'react-router-dom';
import { useRecoilValue } from "recoil";
import { useRecoilValue, useRecoilState } from "recoil";
import * as recoilUtil from '../../util/recoilUtil';
import validator from 'validator';
......@@ -11,7 +11,6 @@ import Header from '../../components/Header';
import RegisterPresenter from "./RegisterPresenter";
import { authApi } from '../../api';
import { resourceLimits } from "worker_threads";
// eslint-disable-next-line @typescript-eslint/no-empty-interface
......@@ -20,13 +19,13 @@ interface RegisterProps extends RouteComponentProps {}
const RegisterContainer = (props : RegisterProps) => {
const token = useRecoilValue(recoilUtil.token);
const [loading, setLoading] = useRecoilState(recoilUtil.loading);
const [registerForm, setRegisterForm] = useState<any>({
userId : '',
password : '',
passwordCheck : '',
info : {
doctorLicense : '',
hospitalNm : '',
hospitalAddr : '',
contact : '',
......@@ -34,10 +33,18 @@ const RegisterContainer = (props : RegisterProps) => {
doctorNm : '',
},
});
const [doctorInfoFile, setDoctorInfoFile] = useState<FileList | null>(null);
const doctorInfoFile_Select = useRef(null);
const [page, setPage] = useState<number>(1);
const [error, setError] = useState<string | null>(null);
const [searchHospital, setSearchHospital] = useState<boolean>(false);
const [hospitalNm, setHospitalNm] = useState<string>('');
const [hospitalSearchPage, setHospitalSearchPage] = useState<number>(1);
const [hospitalSearchPageList, setHospitalSearchPageList] = useState<number[]>([1]);
const [hospitalList, setHospitalList] = useState<any[]>([]);
const [selectHospital, setSelectHospital] = useState<any>(null);
const fetchData = async() => {
......@@ -61,7 +68,6 @@ const RegisterContainer = (props : RegisterProps) => {
}
};
const validateRegisterForm = () => {
if(page === 1) {
if (!validator.isEmail(registerForm.userId)) {
......@@ -74,9 +80,8 @@ const RegisterContainer = (props : RegisterProps) => {
setError('비밀번호가 일치하지 않습니다.')
} else setError(null);
} else if(page === 2) {
if(!registerForm.info.doctorLicense.length &&
!validator.isAlphanumeric(registerForm.info.doctorLicense)) {
setError('의사 자격 번호를 입력해야 합니다.');
if(!doctorInfoFile) {
setError('의사 자격 인증 파일을 첨부해야 합니다.');
} else if(registerForm.info.doctorNm.length < 2) {
setError('의사 이름을 올바르게 입력해야 합니다.');
} else if(!registerForm.info.contact) {
......@@ -116,33 +121,11 @@ const RegisterContainer = (props : RegisterProps) => {
};
const onSetDoctorLicense = (e : React.ChangeEvent<HTMLInputElement>) => {
setRegisterForm({
...registerForm,
info : {
...registerForm.info,
doctorLicense : e.target.value,
},
});
setDoctorInfoFile(e.target.files);
};
const onSetHospitalNm = (e : React.ChangeEvent<HTMLInputElement>) => {
setRegisterForm({
...registerForm,
info : {
...registerForm.info,
hospitalNm : e.target.value,
},
});
};
const onSetHospitalAddr = (e : React.ChangeEvent<HTMLInputElement>) => {
setRegisterForm({
...registerForm,
info : {
...registerForm.info,
hospitalAddr : e.target.value,
},
});
setHospitalNm(e.target.value);
};
const onSetContact = (e : React.ChangeEvent<HTMLInputElement>) => {
......@@ -175,6 +158,56 @@ const RegisterContainer = (props : RegisterProps) => {
});
};
const onSearchHospital = async () => {
try {
setLoading(true);
setSearchHospital(true);
const result = await authApi.searchHospital(hospitalNm, hospitalSearchPage);
if(result.statusText === 'OK') {
setLoading(false);
setHospitalSearchPageList(new Array(result.data.totalPage).fill(null).map((item : null, index : number) => index + 1));
setHospitalList(result.data.hospitalList.length ? result.data.hospitalList : [result.data.hospitalList]);
}
} catch(e : any) {
setLoading(false);
Alert.onError('알 수 없는 에러로 검색에 실패했습니다.', () => null);
}
};
const onSetSearchPrevPage = () => {
//set Prev Page
const pageSlice = 5;
if(hospitalSearchPage > pageSlice) {
setHospitalSearchPage(Math.floor((hospitalSearchPage - 1) / pageSlice) * pageSlice);
}
};
const onSetSearchNextPage = () => {
//set Next Page
const pageSlice = 5;
if(hospitalSearchPage <= Math.floor((hospitalSearchPageList.length - 1) / pageSlice) * pageSlice) {
setHospitalSearchPage(Math.ceil(hospitalSearchPage / pageSlice) * pageSlice + 1);
}
};
const onConfirmSelectHospital = () => {
setSearchHospital(false);
setHospitalSearchPage(1);
setHospitalSearchPageList([1]);
setHospitalList([]);
};
const onCancelSelectHospital = () => {
Alert.onCheck('병원 등록이 취소됩니다. 계속하시겠습니까?', () => {
setSearchHospital(false);
setHospitalNm('');
setHospitalSearchPage(1);
setHospitalSearchPageList([1]);
setHospitalList([]);
setSelectHospital(null);
}, () => null);
};
const onSubmitButton = () => {
if(error) {
Alert.onError(error, () => null);
......@@ -186,26 +219,80 @@ const RegisterContainer = (props : RegisterProps) => {
} else if(page === 2) {
setPage(3);
} else if(page === 3) {
const Data = new FormData();
Data.append('userId', registerForm.userId);
Data.append('password', registerForm.password);
Data.append('passwordCheck', registerForm.passwordCheck);
Data.append('hospitalNm', registerForm.info.hospitalNm);
Data.append('hospitalAddr', registerForm.info.hospitalAddr);
Data.append('contact', registerForm.info.contact);
Data.append('doctorNm', registerForm.info.doctorNm);
Data.append('doctorType', registerForm.info.doctorType);
Data.append('doctorInfoFile', doctorInfoFile ? doctorInfoFile[0] : '');
const onRegisterDoctor = async () => {
//로딩 진행
setLoading(true);
try {
const result = await authApi.registerDoctor(registerForm);
const result = await authApi.registerDoctor(Data);
if(result.data === 'Created') {
setLoading(false);
Alert.onSuccess('회원가입 성공, 관리자의 승인을 대기하세요.', () => props.history.push('/login'));
}
} catch(e) {
} catch(e : any) {
setLoading(false);
Alert.onError(e.response.data.error, () => null);
}
};
if(selectHospital) {
Alert.onCheck('입력하신 정보로 회원가입을 진행하시겠습니까?', onRegisterDoctor, () => null);
} else {
Alert.onError('검색 버튼을 눌러 병원을 선택해주세요.', () => null);
}
}
};
useEffect(() => {
validateRegisterForm();
}, [registerForm, page]);
}, [registerForm, doctorInfoFile, page]);
useEffect(() => {
if(selectHospital) {
setHospitalNm(selectHospital.yadmNm);
setRegisterForm({
...registerForm,
info : {
...registerForm.info,
hospitalNm : selectHospital.yadmNm,
hospitalAddr : selectHospital.addr,
},
});
} else {
setHospitalNm('');
setRegisterForm({
...registerForm,
info : {
...registerForm.info,
hospitalNm : '',
hospitalAddr : '',
},
});
}
}, [selectHospital]);
useEffect(() => {
if(searchHospital) onSearchHospital();
}, [hospitalSearchPage]);
useEffect(() => {
fetchData();
......@@ -218,6 +305,8 @@ const RegisterContainer = (props : RegisterProps) => {
<Header {...props}/>
<RegisterPresenter
registerForm = {registerForm}
doctorInfoFile = {doctorInfoFile}
doctorInfoFile_Select = {doctorInfoFile_Select}
page = {page}
error = {error}
......@@ -228,12 +317,28 @@ const RegisterContainer = (props : RegisterProps) => {
onSetPassword = {onSetPassword}
onSetPasswordCheck = {onSetPasswordCheck}
onSetDoctorLicense = {onSetDoctorLicense}
hospitalNm = {hospitalNm}
onSetHospitalNm = {onSetHospitalNm}
onSetHospitalAddr = {onSetHospitalAddr}
onSetContact = {onSetContact}
onSetDoctorType = {onSetDoctorType}
onSetDoctorNm = {onSetDoctorNm}
onSubmitButton = {onSubmitButton}
searchHospital = {searchHospital}
setSearchHospital = {setSearchHospital}
onSearchHospital = {onSearchHospital}
hospitalSearchPage = {hospitalSearchPage}
setHospitalSearchPage = {setHospitalSearchPage}
hospitalSearchPageList = {hospitalSearchPageList}
onSetSearchPrevPage = {onSetSearchPrevPage}
onSetSearchNextPage = {onSetSearchNextPage}
onConfirmSelectHospital = {onConfirmSelectHospital}
onCancelSelectHospital = {onCancelSelectHospital}
hospitalList = {hospitalList}
selectHospital = {selectHospital}
setSelectHospital = {setSelectHospital}
/>
</>
)
......
import React from 'react';
import Modal from '../../components/Modal';
import * as styled from './RegisterStyled';
const lensImg = '/static/img/lens.png';
const check = '/static/img/check.png';
const uncheck = '/static/img/uncheck.png'
const next = '/static/img/next.png';
const prev = '/static/img/prev.png';
interface RegisterProps {
registerForm : {
userId : string;
password : string;
passwordCheck : string;
info : {
doctorLicense : string;
hospitalNm : string;
hospitalAddr : string;
contact : string;
......@@ -17,6 +24,8 @@ interface RegisterProps {
doctorNm : string;
},
};
doctorInfoFile : FileList | null;
doctorInfoFile_Select : any;
page : number;
error : string | null;
......@@ -27,18 +36,122 @@ interface RegisterProps {
onSetPassword : React.ChangeEventHandler<HTMLInputElement>;
onSetPasswordCheck : React.ChangeEventHandler<HTMLInputElement>;
onSetDoctorLicense : React.ChangeEventHandler<HTMLInputElement>;
hospitalNm : string;
onSetHospitalNm : React.ChangeEventHandler<HTMLInputElement>;
onSetHospitalAddr : React.ChangeEventHandler<HTMLInputElement>;
onSetContact : React.ChangeEventHandler<HTMLInputElement>;
onSetDoctorType : React.ChangeEventHandler<HTMLInputElement>;
onSetDoctorNm : React.ChangeEventHandler<HTMLInputElement>;
onSubmitButton : () => void;
searchHospital : boolean;
setSearchHospital : (arg0 : boolean) => void;
onSearchHospital : () => void;
hospitalSearchPage : number;
setHospitalSearchPage : (arg0 : number) => void;
hospitalSearchPageList : number[];
onSetSearchPrevPage : () => void;
onSetSearchNextPage : () => void;
onConfirmSelectHospital : () => void;
onCancelSelectHospital : () => void;
hospitalList : any[];
selectHospital : any;
setSelectHospital : (arg0 : any) => void;
}
const RegisterPresenter = (props : RegisterProps) => {
return (
<styled.Container>
{
props.searchHospital ?
<Modal onModalClose = {props.onCancelSelectHospital}>
<>
<styled.SearchTitle>
{`[${props.hospitalNm}] 에 대한 검색 결과 : `}
<styled.SearchResultCount style = {{marginLeft : 5, marginRight : 5,}}>총 </styled.SearchResultCount>
{props.hospitalSearchPageList.length}
<styled.SearchResultCount>페이지</styled.SearchResultCount>
</styled.SearchTitle>
<styled.HospitalListWrapper>
<styled.HospitalListInfo>
<styled.HospitalListInfoEach isLast = {false}>이름</styled.HospitalListInfoEach>
<styled.HospitalListInfoEach isLast = {false}>주소</styled.HospitalListInfoEach>
<styled.HospitalListInfoEach isLast = {true}>선택</styled.HospitalListInfoEach>
</styled.HospitalListInfo>
{
props.hospitalList.map((hospital : any) => {
return (
<styled.HospitalListEach
key = {hospital.addr}
>
<styled.HospitalListEachInfo isLast = {false}>
{hospital.yadmNm}
</styled.HospitalListEachInfo>
<styled.HospitalListEachInfo isLast = {false}>
{hospital.addr}
</styled.HospitalListEachInfo>
<styled.HospitalListEachInfo isLast = {true}>
<styled.CheckButton
onClick = {() => props.setSelectHospital(hospital)}
>
<styled.CheckButtonImg src = {
props.selectHospital && props.selectHospital.addr === hospital.addr ?
check : uncheck
}/>
</styled.CheckButton>
</styled.HospitalListEachInfo>
</styled.HospitalListEach>
)
})
}
</styled.HospitalListWrapper>
<styled.PageWrapper>
<styled.PageButton
isSelect = {false}
onClick = {props.onSetSearchPrevPage}
>
<styled.PageArrowImg src = {prev}/>
</styled.PageButton>
{
props.hospitalSearchPageList.slice(Math.floor((props.hospitalSearchPage - 1) / 5) * 5, Math.floor((props.hospitalSearchPage - 1) / 5) * 5 + 5)
.map((page : number) => {
return (
<styled.PageButton
key = {page}
isSelect = {props.hospitalSearchPage === page}
onClick = {() => props.setHospitalSearchPage(page)}
>
{page}
</styled.PageButton>
)
})
}
<styled.PageButton
isSelect = {false}
onClick = {props.onSetSearchNextPage}
>
<styled.PageArrowImg src = {next}/>
</styled.PageButton>
</styled.PageWrapper>
<styled.ModalButtonWrapper>
<styled.ModalButton
isCloseButton = {false}
onClick = {props.onConfirmSelectHospital}
>
확인
</styled.ModalButton>
<styled.ModalButton
isCloseButton = {true}
onClick = {props.onCancelSelectHospital}
>
취소
</styled.ModalButton>
</styled.ModalButtonWrapper>
</>
</Modal> : null
}
<styled.RegisterWrapper>
<styled.RegisterBackButtonWrapper>
<styled.RegisterBackButton
......@@ -92,11 +205,19 @@ const RegisterPresenter = (props : RegisterProps) => {
<>
<styled.RegisterInputWrapper>
<styled.RegisterInputText>의사 자격증 번호</styled.RegisterInputText>
<styled.RegisterInput
placeholder = "Doctor's License"
value = {props.registerForm.info.doctorLicense}
<input type = 'file'
style = {{ display : 'none' }}
onChange = {props.onSetDoctorLicense}
ref = {props.doctorInfoFile_Select}
/>
<styled.RegisterFileUploadWrapper>
<styled.RegisterFileUploadButton onClick = {() => props.doctorInfoFile_Select.current.click()}>
파일 첨부
</styled.RegisterFileUploadButton>
<styled.RegisterFileUploadInfoText>
{props.doctorInfoFile ? props.doctorInfoFile[0].name : ''}
</styled.RegisterFileUploadInfoText>
</styled.RegisterFileUploadWrapper>
</styled.RegisterInputWrapper>
<styled.RegisterInputWrapper>
<styled.RegisterInputText>이름</styled.RegisterInputText>
......@@ -118,27 +239,33 @@ const RegisterPresenter = (props : RegisterProps) => {
props.page === 3 ?
<>
<styled.RegisterInputWrapper>
<styled.RegisterInputText>전문 분야</styled.RegisterInputText>
<styled.RegisterInput
placeholder = "Doctor's Type"
value = {props.registerForm.info.doctorType}
onChange = {props.onSetDoctorType}
/>
</styled.RegisterInputWrapper>
<styled.RegisterInputWrapper>
<styled.RegisterInputText>병원 이름</styled.RegisterInputText>
<styled.RegisterInputText>병원</styled.RegisterInputText>
<styled.RegisterInputWrapperForSearch>
<styled.RegisterInput
placeholder = 'Hospital'
value = {props.registerForm.info.hospitalNm}
value = {props.hospitalNm}
onChange = {props.onSetHospitalNm}
/>
<styled.RegisterInputSearchButton
onClick = {props.onSearchHospital}
>
<styled.RegisterInputSearchButtonImg src = {lensImg}/>
</styled.RegisterInputSearchButton>
</styled.RegisterInputWrapperForSearch>
</styled.RegisterInputWrapper>
<styled.RegisterInputWrapper>
<styled.RegisterInputText>병원 주소</styled.RegisterInputText>
<styled.RegisterInputText>주소</styled.RegisterInputText>
<styled.RegisterInput
placeholder = 'Address'
value = {props.registerForm.info.hospitalAddr}
onChange = {props.onSetHospitalAddr}
/>
</styled.RegisterInputWrapper>
<styled.RegisterInputWrapper>
<styled.RegisterInputText>전문 분야</styled.RegisterInputText>
<styled.RegisterInput
placeholder = "Doctor's Type"
value = {props.registerForm.info.doctorType}
onChange = {props.onSetDoctorType}
/>
</styled.RegisterInputWrapper>
</> : null
......
......@@ -10,6 +10,198 @@ export const Container = styled.div `
align-items : center;
`;
export const SearchTitle = styled.div `
font-weight : 600;
font-size : 20;
color : #337DFF;
display : flex;
flex-direction : row;
align-items : center;
justify-content : center;
`;
export const SearchResultCount = styled.div `
color : #343434;
`;
export const HospitalListWrapper = styled.div `
margin : 20px 0;
height : 200px;
width : 80%;
border : 1px solid #337DFF;
border-radius : 3px;
display : flex;
flex-direction : column;
`;
export const HospitalListInfo = styled.div `
height : 25px;
width : 100%;
border : none;
border-bottom : 2px solid #ddd;
display : flex;
flex-direction : row;
`;
export const HospitalListInfoEach = styled.div<{isLast : boolean}> `
flex : ${props => props.isLast ? '1' : '3'};
display : flex;
align-items : center;
justify-content : center;
font-size : 14px;
font-weight : 500;
color : #343434;
border : none;
border-right : ${props => props.isLast ? 'none' : '1px solid #ddd'};
padding : 3px 5px;
`;
export const HospitalListEach = styled.div `
min-height : 34px;
max-height : 34px;
width : 100%;
display : flex;
flex-direction : row;
border : none;
border-bottom : 1px solid #ddd;
`;
export const HospitalListEachInfo = styled.div<{isLast : boolean}> `
flex : ${props => props.isLast ? '1' : '3'};
display : flex;
font-size : 12px;
font-weight : 500;
justify-content : center;
align-items : center;
border : none;
border-right : ${props => props.isLast ? 'none' : '1px solid #ddd'};
padding : 3px 5px;
`;
export const CheckButton = styled.button `
border : none;
background-color : transparent;
height : 15px;
width : 15px;
display : flex;
flex-direction : row;
justify-content : center;
cursor : pointer;
transition : .25s all;
&:hover {
opacity : .5;
}
`;
export const CheckButtonImg = styled.img `
height : 15px;
width : 15px;
`;
export const PageWrapper = styled.div `
width : 50%;
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
gap : 2%;
`;
export const PageButton = styled.button<{isSelect : boolean}> `
height : 18px;
width : 18px;
display : flex;
align-items : center;
justify-content : center;
border : none;
border-radius : 4px;
background-color : ${props => props.isSelect ? '#337DFF' : 'transparent'};
color : ${props => props.isSelect ? '#fff' : '#343434'};
font-size : 12px;
font-weight : 600;
cursor : pointer;
transition : .25s all;
&:hover {
opacity : .7;
}
`;
export const PageArrowImg = styled.img `
height : 15px;
width : 15px;
`;
export const ModalButtonWrapper = styled.div `
margin : 20px 0 0 0;
width : 50%;
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
border : none;
gap : 10%;
`;
export const ModalButton = styled.div<{isCloseButton : boolean}> `
padding : 2.5% 10%;
cursor : pointer;
border : 1px solid ${props => props.isCloseButton ? '#343434' : '#337DFF'};
background-color : ${props => props.isCloseButton ? 'transparent' : '#337DFF'};
border-radius : 5px;
color : ${props => props.isCloseButton ? '#343434' : '#fff'};
font-weight : 600;
font-size : 16px;
transition : .25s all;
&:hover {
opacity : .7;
}
`
export const RegisterWrapper = styled.div `
width : 35%;
border : none;
......@@ -24,10 +216,9 @@ export const RegisterWrapper = styled.div `
padding : 30px 3px;
box-shadow: 0px 0px 10px #a0a0a0;
`;
export const RegisterBackButtonWrapper = styled.div `
width : 100%;
border : none;
......@@ -97,6 +288,18 @@ export const RegisterInputText = styled.div `
`;
export const RegisterInputWrapperForSearch = styled.div `
display : flex;
flex-direction : row;
justify-content : center;
width : 100%;
border : none;
background-color : transparent;
`;
export const RegisterInput = styled.input `
width : 80%;
padding : 5px 10px;
......@@ -111,6 +314,87 @@ export const RegisterInput = styled.input `
}
`;
export const RegisterFileUploadWrapper = styled.div `
width : 80%;
display : flex;
flex-direction : row;
justify-content : flex-start;
align-items : center;
border : none;
background-color : transparent;
`;
export const RegisterFileUploadButton = styled.button `
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
border-radius : 3px;
border : 1px solid #343434;
background-color : transparent;
color : #343434;
padding : 3px 4px;
font-size : 12px;
font-weight : 600;
cursor : pointer;
transition : .25s all;
&:hover {
border : 1px solid #337DFF;
color : #fff;
background-color : #337DFF;
}
margin : 0 5% 0 0;
`;
export const RegisterFileUploadInfoText = styled.div `
font-size : 12px;
font-weight : 600;
color : #337DFF;
`;
export const RegisterInputSearchButton = styled.button `
position : absolute;
height : 25px;
width : 25px;
align-self : end;
margin : 0 0 1px 24%;
background-color : transparent;
border : none;
transition : .25s all;
&:hover {
opacity : .5;
}
display : flex;
flex-direction : row;
justify-content : center;
align-items : center;
cursor : pointer;
`;
export const RegisterInputSearchButtonImg = styled.img `
height : 20px;
width : 20px;
`;
export const RegisterButtonWrapper = styled.div `
margin : 20px 0 0 0;
......
This diff could not be displayed because it is too large.