'use strict' /** * Copyright (c) 2017~2020, OBCon Inc. * All rights reserved. */ /** * @file * @copyright 2017~2020, OBCon Inc. * @author gye hyun james kim [pnuskgh@gmail.com] */ let fs = require('fs'); let path = require('path'); let crypto = require('crypto'); let moment = require('moment'); //--- format : YYYYMMDDHHmmss.SSS ZZ - 20191220172919.083 +0900 //--- Full Path를 반환 한다. function getPath() { return path.resolve(config.folder_root, path.join.apply(this, arguments)); } //--- Custom Path를 반환 한다. function getCustomPath() { let filePath = path.join.apply(this, arguments); let files = [ getPath('custom', config.service.name, filePath), getPath('custom', filePath), getPath(filePath) ]; let rtFile = getPath(filePath); for (let idx = 0; idx < files.length; idx++) { if (fs.existsSync(files[idx])) { rtFile = files[idx]; break; } } return path.resolve(rtFile); } function existFile() { let filePath = path.join.apply(this, arguments); let files = [ getPath('custom', config.service.name, filePath), getPath('custom', filePath), getPath(filePath) ]; let rtFlag = false; for (let idx = 0; idx < files.length; idx++) { if (fs.existsSync(files[idx])) { rtFlag = true; break; } } return rtFlag; } function getClass() { let filePath = getCustomPath.apply(this, arguments); return (fs.existsSync(filePath)) ? require(filePath):null; } function getModule() { let moduleClass = getClass.apply(this, arguments); return (moduleClass == null) ? null:new moduleClass(); } // function getSettingModule() { // let filePath = getCustomPath.apply(this, arguments); // let moduleClass = require(filePath); // return new moduleClass(); // } //--- 전체 URL을 반환 한다. function getFullUrl(url='') { // let rturl = config.http.baseUrl + config.http.path.substring(1); return (url == '') ? config.http.baseUrl:config.http.baseUrl + url.substring(1); } function getTheme(req) { let theme = JSON.parse(JSON.stringify(config.theme)); //--- Nginx에서 호출되는 경우 사용 // if (req.headers.hasOwnProperty('x-forwarded-for')) { // theme.templateBase = '/themes/' + config.theme.name + '/public'; // } else { // theme.templateBase = ''; // } theme['http'] = config.http; return theme; } //--- To-Do : 향후 site, user별 설정을 가져 간다. //--- Theme Customize를 위한 함수 : getTemplatePath() //--- modbus_ws_app.js의 app.set('views', getPath('themes'));와 app.use(express.static(getPath('themes'))); 참조 //--- app.use(express.static(getPath('themes'))); 설정에 의해 themes/ 폴더가 기본 폴더가 됨 function _getTemplateFile() { let filePath = path.join.apply(this, arguments); let files = [ getPath('themes', config.service.name, config.theme.name, filePath), getPath('themes', config.service.name, config.theme.defaultName, filePath), getPath('themes', config.theme.name, filePath), getPath('themes', config.theme.defaultName, filePath) ]; let urls = [ path.join(config.service.name, config.theme.name, filePath), path.join(config.service.name, config.theme.defaultName, filePath), path.join(config.theme.name, filePath), path.join(config.theme.defaultName, filePath) ]; // if (-1 < filePath.indexOf('spaAll')) { // console.log('filePath', filePath); // console.log('getTemplateFile', files); // } for (let idx = 0; idx < files.length; idx++) { if (fs.existsSync(files[idx])) { // if (-1 < filePath.indexOf('spaAll')) { // console.log(urls[idx]); // } return urls[idx].replace(/\\/g, '/'); } } return path.join('themes', config.theme.defaultName, filePath).replace(/\\/g, '/'); } function getTemplateFile() { return '/' + _getTemplateFile.apply(this, arguments); } //--- ejs에서 파일을 링크하기 위해서 사용 한다. function getTemplateFile2() { if ((arguments.length == 1) && (arguments[0].startsWith('http'))) { return arguments[0]; } return config.http.path + '/' + _getTemplateFile.apply(this, arguments); } //--- app.set('views', getPath('themes')); 설정에 의해 theme/ 폴더가 기본 폴더가 됨 function getTemplateEjs() { let file = _getTemplateFile.apply(this, arguments); return file; // return file.substr(0, file.length - 4); } function stringFormat(valStr, len, pad=' ') { let rtStr = valStr; while (rtStr.length < len) { rtStr = pad + rtStr; } return rtStr; } function stringLength(str) { let len = 0; for (let idx = 0; idx < str.length; idx++) { if (escape(str.charAt(idx)).length == 6) { len++; } len++; } return len; } function integerFormat(valInt, len, pad=' ', abs=true) { let rtStr = valInt.toString(); if (-1 < rtStr.indexOf('.')) { rtStr = rtStr.split('.')[0]; } while (rtStr.length < len) { rtStr = pad + rtStr; } if ((abs) && (len < rtStr.length)) { rtStr = rtStr.slice(rtStr.length - len, rtStr.length); } return rtStr; } //--- To-Do : 향후 개선 한다. //--- abs : true. 소수점 아래와 소수점 위의 길이를 지킨다. function floatFormat(valFloat, lenFirst=5, lenSecond=2, padFirst=' ', padSecond='0', abs=true) { let rtStr = valFloat.toString(); let tmpFirst = null; let tmpSecond = null; if (-1 < rtStr.indexOf('.')) { let items = rtStr.split('.'); tmpFirst = items[0]; tmpSecond = items[1]; if (abs) { if (lenSecond < tmpSecond.length) { tmpSecond = tmpSecond.slice(0, lenSecond); } } } else { tmpFirst = rtStr; tmpSecond = ''; } while (tmpSecond.length < lenSecond) { tmpSecond = tmpSecond + padSecond; } rtStr = tmpFirst + '.' + tmpSecond; while (rtStr.length < (lenFirst + 1 + lenSecond)) { rtStr = padFirst + rtStr; } if ((abs) && ((lenFirst + 1 + lenSecond) < rtStr.length)) { rtStr = rtStr.slice(rtStr.length - (lenFirst + 1 + lenSecond), rtStr.length); } return rtStr; } //--- 소수점 아래 자리만 확정 한다. function floatFormat2(valFloat, lenSecond=2, padSecond='0') { let rtStr = valFloat.toString(); let tmpFirst = null; let tmpSecond = null; if (-1 < rtStr.indexOf('.')) { let items = rtStr.split('.'); tmpFirst = items[0]; tmpSecond = items[1]; if (lenSecond < tmpSecond.length) { tmpSecond = tmpSecond.slice(0, lenSecond); } } else { tmpFirst = rtStr; tmpSecond = ''; } while (tmpSecond.length < lenSecond) { tmpSecond = tmpSecond + padSecond; } rtStr = tmpFirst + '.' + tmpSecond; return rtStr; } function getHash(plainText) { let hash = crypto.createHash('md5'); hash.update(plainText); return hash.digest('hex').toLowerCase(); } //--- string, number, bigint, boolean, function, undefined, symbol //--- object : null, object, array function obconTypeof(param) { return Object.prototype.toString.call(param).replace(/\[object /g, '').replace(/\]/g, '').toLowerCase(); } function Null(value) { if (isNone(value)) { return ''; } else if (value == null) { return ''; } return value; } //--- typeof : string (String), bigint (BigInt), number (Number), boolean (Boolean), object (Null), symbol (Symbol), function, undefined (Undefined) function isNone(param1) { return typeof(param1) === 'undefined'; } function isNull(param1) { return param1 == null; } function isString(param1) { return typeof(param1) == 'string'; } function isNumber(param1) { return typeof(param1) == 'number'; } function isArray(param1) { return obconTypeof(param1) == 'array'; } function existField(paramObject, paramField) { if ((paramObject == null) || (typeof(paramObject) == 'undefined')) { return false; } return typeof(paramObject[paramField]) != 'undefined'; // return Object.getOwnPropertyNames(paramObject).includes(paramField); // return -1 < Object.getOwnPropertyNames(paramObject).indexOf(paramField); } function existIdxField(paramObject, paramField) { if ((paramObject == null) || (typeof(paramObject) == 'undefined')) { return false; } return typeof(paramObject[`idx_${paramField}`]) != 'undefined'; // return Object.getOwnPropertyNames(paramObject).includes(paramField); // return -1 < Object.getOwnPropertyNames(paramObject).indexOf(paramField); } function getObjectValue(paramObject, paramField, paramDefault) { return existField(paramObject, paramField) ? paramObject[paramField]:paramDefault; } function deepCopy(obj) { return JSON.parse(JSON.stringify(obj)); } function randomInteger(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function randomFloat(min, max) { return Math.random() * (max - min) + min; } //--- pos : 1(첫번째 문자), 2(두번째 문자), ..., 32 let maxPos = 32; function initFlagString(source) { let tmpStr = source + '0000000000000000000000000000000000000000'; return tmpStr.substring(0, maxPos); } function getFlagString(source, pos) { let tmpStr = initFlagString(source); tmpStr = tmpStr.substring(pos - 1, pos); return tmpStr; } function setFlagString(source, pos, set) { let tmpStr = initFlagString(source); tmpStr = tmpStr.substring(0, pos - 1) + set + tmpStr.substring(pos, maxPos); return tmpStr; } //--- pos : 1(일자리), 2(십자리), ..., 32 function getFlagNumber(source, pos) { let tmpStr = '0000000000000000' + Math.abs(source); tmpStr = tmpStr.substring(tmpStr.length - 16); tmpStr = tmpStr.substring(16 - pos, 16 - pos + 1); return parseInt(tmpStr); } function setFlagNumber(source, pos, set) { let tmpStr = '0000000000000000' + Math.abs(source); tmpStr = tmpStr.substring(tmpStr.length - 16); tmpStr = tmpStr.substring(0, 16 - pos) + set + tmpStr.substring(16 - pos + 1); return parseInt(tmpStr); } function checkHacking(param) { if (!param) { return false; } if ((-1 < param.indexOf(';')) || (-1 < param.indexOf('\''))) { return true; } return false; } //--- 박광서의 요청에 따라 현재 시간이 아니라, 전달되는 데이터의 시간으로 체크 한다. 2019.03.31 //--- 통신 이상 검사 : true. 통신 이상, false. 통신 정상 //--- //--- 한번도 데이터를 받은 적이 없으면 통신 이상 //--- 1. 정류기: 설정한 시간(Default 60초) 이상 데이터가 입력되지 않으면 통신 이상 //--- 2. 원격TB: 통신 주기 * 3 보다 통신이 되지 않은 시간이 클 경우 통신 이상 //--- 3. 배관링크: 통신 주기 * 3 보다 통신이 되지 않은 시간이 클 경우 통신 이상 //--- 4. 수위센서: 통신 주기 * 3 보다 통신이 되지 않은 시간이 클 경우 통신 이상 //--- 5. 관말압력 //--- 피크타임 모드인 경우 //--- 피크타임인 경우 - 통신 주기 * 3 보다 통신이 되지 않은 시간이 클 경우 통신 이상 //--- 피크타임이 아닌 경우 - 항상 통신 정상 //--- 피크타임 모드가 아닌 경우 - 통신 주기 * 3 보다 통신이 되지 않은 시간이 클 경우 통신 이상 //--- 6. 특정정압기: 통신 주기 * 3 보다 통신이 되지 않은 시간이 클 경우 통신 이상 //--- 7. 수신감도: 통신 주기 * 3 보다 통신이 되지 않은 시간이 클 경우 통신 이상 // function isNetworkError(deviceIndex, type, networkCycle, networkCycleUnit, statusDatetime) { function isNetworkError(siteKey, device, statusDatetime) { let deviceIndex = siteKey + '_' + device.type + '_' + device.deviceKey; let type = device.type; let networkCycle = device.networkCycle; let networkCycleUnit = device.networkCycleUnit; if (type == '5') { //--- 5. 관말압력 if (device.networkMode == '4') { //--- 4. 피크타임 모드 networkCycle = device.peakNetworkCycle; networkCycleUnit = '분'; //--- 설정된 피크타임 시간이 아니면, statusDatetime을 현재 시간으로 설정하여 항상 통신 이상이 발생하지 않도록 한다. let isPeak = false; let hour = parseInt(moment().format('HH')); //--- 현재 시간 (0 ~ 24) if ((device.peakTimeStart1 <= hour) && (hour < device.peakTimeEnd1)) { isPeak = true; } if ((device.peakTimeStart2 <= hour) && (hour < device.peakTimeEnd2)) { isPeak = true; } if (isPeak == false) { statusDatetime = moment(); } } } //--- 통신 이상 여부를 검사 한다. let rtcd = true; if (statusDatetime == null) { //--- 한번도 데이터를 받은 적이 없으면 통신 이상 처리 return true; } let datetimeFr = statusDatetime; //--- 최종 도착한 데이터의 시간 let datetimeTo = moment(); //--- 현재 시간 let duration = 0; let unit = '분'; switch (type) { case '1' : unit = '초'; duration = moment.duration(datetimeTo.diff(datetimeFr)).asSeconds(); //--- 정류기는 60초 이상 데이터가 입력되지 않으면 통신 이상 처리 -> 설정한 시간으로 비교 // rtcd = (60 < duration); rtcd = (device.networkCheckTime < duration); logger.info(`include/global_utils.js :: ${deviceIndex} : Duration - ${floatFormat(duration, 7, 2)} ${unit}, networkCycle : ${device.networkCheckTime} :: ${rtcd}`); break; case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : unit = '분'; duration = moment.duration(datetimeTo.diff(datetimeFr)).asMinutes(); if (networkCycleUnit == '시') { networkCycle = networkCycle * 60; } //--- 박광서의 요청으로 임시로 통신 주기 * 3 보다 통신이 되지 않은 시간이 클 경우 오류 처리 (2019-03-11) rtcd = (3 * networkCycle < duration); logger.info(`include/global_utils.js :: ${deviceIndex} : Duration - ${floatFormat(duration, 7, 2)} ${unit}, 3 * networkCycle : ${3 * networkCycle} :: ${rtcd}`); break; case '8': case 'A': default: switch (networkCycleUnit) { case '초': unit = '초'; duration = moment.duration(datetimeTo.diff(datetimeFr)).asSeconds(); break; case '분': unit = '분'; duration = moment.duration(datetimeTo.diff(datetimeFr)).asMinutes(); break; case '시': unit = '분'; duration = moment.duration(datetimeTo.diff(datetimeFr)).asMinutes(); networkCycle = networkCycle * 60; break; } rtcd = (3 * networkCycle < duration); logger.info(`include/global_utils.js :: ${deviceIndex} : Duration - ${floatFormat(duration, 7, 2)} ${unit}, 3 * networkCycle : ${3 * networkCycle} :: ${rtcd}`); break; } return rtcd; } function saveNotificationNormal(device, datetimeTo) { let noti = {}; noti.name = '통신 정상'; noti.site = device.site; noti.device = device.id; noti.deviceKey = device.deviceKey; noti.deviceName = device.name; noti.branch = -1; noti.branchName = ''; noti.type = device.type; noti.eventDatetime = datetimeTo.format('YYYY-MM-DD HH:mm:ss'); noti.level = 'Info'; noti.description = '통신 정상'; //--- branch로 branch 이름을 가져 온다. let query = { where: { id: device.branch, deleted: false } }; modules.branches.model.table.findOne(query) .then(function(branch) { if (branch != null) { noti.branch = branch.id; noti.branchName = branch.name; } modules.notifications.model.table.create(noti) .then(function(results) { // logger.info(JSON.stringify(results)); }).catch(utils.obj.loggerError); }).catch(function(err) { logger.error(JSON.stringify(err)); modules.notifications.model.table.create(noti) .then(function(results) { // logger.info(JSON.stringify(results)); }).catch(utils.obj.loggerError); }); } function saveNotificationError(device, datetimeTo) { let noti = {}; noti.name = '통신 이상'; noti.site = device.site; noti.device = device.id; noti.deviceKey = device.deviceKey; noti.deviceName = device.name; noti.branch = -1; noti.branchName = ''; noti.type = device.type; noti.eventDatetime = datetimeTo.format('YYYY-MM-DD HH:mm:ss'); noti.level = 'Error'; noti.description = '통신 이상'; //--- branch로 branch 이름을 가져 온다. let query = { where: { id: device.branch, deleted: false } }; modules.branches.model.table.findOne(query) .then(function(branch) { if (branch != null) { noti.branch = branch.id; noti.branchName = branch.name; } modules.notifications.model.table.create(noti) .then(function(results) { // logger.info(JSON.stringify(results)); }).catch(utils.obj.loggerError); }).catch(function(err) { logger.error(JSON.stringify(err)); modules.notifications.model.table.create(noti) .then(function(results) { // logger.info(JSON.stringify(results)); }).catch(utils.obj.loggerError); }); } function stringToHex(str) { const buf = Buffer.from(str, 'utf8'); return buf.toString('hex'); } function hexToString(str) { const buf = new Buffer(str, 'hex'); return buf.toString('utf8'); } function isWindows() { return (process.env.HOMEDRIVE) ? true:false; } function isLinux() { return (process.env.HOME) ? true:false; } function stockUnitPrice(price) { if (price < 1000) { return 1; } else if (price < 5000) { return 5; } else if (price < 10000) { return 10; } else if (price < 50000) { return 50; } else if (price < 100000) { return 100; } else if (price < 500000) { return 500; } else { return 1000; } } //--- 주문가격으로 판매 가격 계산 : ROUNDUP(R350 * (1 + 1%) / H350, 0) * H350 function stockCalcSalePrice(stockCode, orderPrice, saleRate = 1.0) { //--- saleRate : 1% 상승시 판매 // let saleRate = self.getPolicy(stockCode, 'saleRate') let salePrice = orderPrice * (1 + saleRate / 100.0); let unit = stockUnitPrice(salePrice); return Math.ceil(salePrice / unit) * unit; } //--- 키움증권 매수 수수료 : ROUNDDOWN(매수 금액 * 0.015%, -1) function stockCalcPurchaseFee(purchaseTotal) { let fee = purchaseTotal * 0.015 / 100 return Math.floor(fee / 10) * 10; } //--- 키움증권 매도 수수료 : ROUNDDOWN(매도 금액 * 0.015%, -1) function stockCalcSaleFee(saleTotal) { let fee = saleTotal * 0.015 / 100; return Math.floor(fee / 10) * 10; } //--- 키움증권 매도 거래세 : ROUND((W1257 - J1257 - X1257) * (0.1% + 0.15%), -1) function stockCalcSaleTax(purchaseTotal, saleTotal) { let fee = (saleTotal - stockCalcPurchaseFee(purchaseTotal) - stockCalcSaleFee(saleTotal)) * (0.1 + 0.15) / 100; return Math.ceil(fee / 10) * 10; } //--- To-Do : LocalStorage, Cookie 등을 사용하여 설정 관리 function makeSetting(id='iconSetting') { // if ((!config.func) || (!config.func.page) || (!config.func.page.setting)) { // return ''; // } // if (config.func.page.setting == false) { // return ''; // } //--- https://icons.getbootstrap.com/ //   return ``; } //--- options.helpPage = 'url'; //--- options.helpPage = [ { name: '', url: '' } ]; //--- To-Do : 선택 목록을 보여준 후 선택시 도움말 화면 표시 function makeHelp(id='iconHelp') { // if ((!config.func) || (!config.func.page) || (!config.func.page.setting)) { // return ''; // } // if (config.func.page.setting == false) { // return ''; // } //--- https://icons.getbootstrap.com/ // return ``; return ``; } function getClientIp(req) { var clientIp = req.body.clientIp || req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress || ''; clientIp = clientIp.split(',')[0]; clientIp = clientIp.split(':').slice(-1)[0]; return clientIp; } function datetimeFormatter(str, formatFr = 'YYYYMMDDHHmmss', formatTo = 'YYYY-MM-DD HH:mm:ss') { return (str == '') ? '': moment(str, formatFr).format(formatTo); } // async function sleep(miliseconds) { // let promise = new Promise(function(resolve, reject) { // setTimeout(function() { // resolve(); // }, miliseconds); // }); // await promise; // } module.exports = { getPath: getPath, getCustomPath: getCustomPath, existFile: existFile, getClass: getClass, getModule: getModule, // getSettingModule: getSettingModule, getFullUrl: getFullUrl, getTheme: getTheme, getTemplateFile: getTemplateFile, getTemplateFile2: getTemplateFile2, getTemplateEjs: getTemplateEjs, getHash: getHash, stringFormat: stringFormat, stringLength: stringLength, integerFormat: integerFormat, floatFormat: floatFormat, floatFormat2: floatFormat2, Null: Null, isNone: isNone, isNull: isNull, isString: isString, isNumber: isNumber, isArray: isArray, existField: existField, existIdxField: existIdxField, getObjectValue: getObjectValue, deepCopy: deepCopy, datetimeFormatter: datetimeFormatter, initFlagString: initFlagString, getFlagString: getFlagString, setFlagString: setFlagString, getFlagNumber: getFlagNumber, setFlagNumber: setFlagNumber, checkHacking: checkHacking, randomInteger: randomInteger, randomFloat: randomFloat, isNetworkError: isNetworkError, saveNotificationNormal: saveNotificationNormal, saveNotificationError: saveNotificationError, stringToHex: stringToHex, hexToString: hexToString, isWindows: isWindows, isLinux: isLinux, makeHelp: makeHelp, makeSetting: makeSetting, getClientIp: getClientIp, // sleep: sleep, stock: { unitPrice: stockUnitPrice, calcSalePrice : stockCalcSalePrice, calcPurchaseFee: stockCalcPurchaseFee, calcSaleFee: stockCalcSaleFee, calcSaleTax: stockCalcSaleTax }, obj: getModule('include', 'obconObject.js') };