간단한 인터페이스의 재고 관리 시스템입니다.
사용자가 가입할 때 등록한 회사별로 재고를 관리 할 수 있습니다.
간단한 인터페이스로 손쉽게 사용 할 수 있으며 쉽게 재고 및 정보를 관리 할 수 있습니다.
🌷 GITHUB : https://github.com/indeeeah/InventoryManagementService
GitHub - indeeeah/InventoryManagementService: 재고 관리 서비스 프로젝트입니다.
재고 관리 서비스 프로젝트입니다. Contribute to indeeeah/InventoryManagementService development by creating an account on GitHub.
github.com
🌷 시연영상 : https://indeeah.tistory.com/46
재고 관리 시스템 시연 영상
version: 1.0.0 기능 회원가입 로그인 비밀번호 변경 카테고리 추가 제품 추가 제품 수정
indeeah.tistory.com
📑 폴더 구조
Frontend
- `src/views/i-${pageName}.html` : 화면
- `src/views/js/`
- `api.js` : api 호출
- `base.js` : 공통으로 쓰이는 함수
- `${pageName}.js` : 화면별 함수
Backend
- `src/controlers/` : Controler
- `${pathName1}/`
- `${pathName1}Handler.js`
- `function/${pathName1}.${pathName2}.js`
- `${pathName1}/`
- `src/lib/`
- `base.js` : 공통으로 쓰이는 함수
- `crypto.js` : 암호화
- `errCode.js` : 에러 코드
- `db.js`, `rds.db.js`, `rds.query.js` : Model
- `src/middlewares/`
- `eventForm.js` : request를 event form으로 convert
- `userToken.js` : jwt
- `src/routers` : router
💻 DB
mysql 연동 에러로 mysql2사용
config
module.exports = {
db: {
rds: {
host: (process.env.INV_RDS_HOST || '127.0.0.1'),
whost: (process.env.INV_RDS_WHOST || '127.0.0.1'),
port: (process.env.INV_RDS_PORT || '3306'),
user: (process.env.INV_RDS_USER || 'devinventory'),
password: (process.env.INV_RDS_PASSWORD || ''),
database: (process.env.INV_RDS_DATABASE || 'devinventory'),
},
}
};
connect
let connectDb = (dbInst) => {
return new Promise((resolve, reject) => {
dbInst.connect((err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
};
end
let disconnectDb = (dbInst) => {
return new Promise((resolve, reject) => {
dbInst.end((err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
};
Transaction query
/*********************************************************
* Transaction
********************************************************/
module.exports.startTransaction = () => {
return `START TRANSACTION`;
};
module.exports.commit = () => {
return `COMMIT`;
};
module.exports.rollback = () => {
return `ROLLBACK`;
};
module.exports.setTimezone = () => {
return `SET time_zone = '+9:00'`;
};
module.exports.getLock = (name, sec) => {
return `SELECT GET_LOCK('${name}', ${sec}) AS db_lock`;
};
module.exports.releaseLock = (name) => {
return `SELECT RELEASE_LOCK('${name}') AS db_lock`;
};
Tables
-- company
CREATE TABLE IF NOT EXISTS company (
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
valid INT(2) UNSIGNED NOT NULL,
dt DATETIME,
description VARCHAR(255),
PRIMARY KEY(id));
-- user
CREATE TABLE IF NOT EXISTS user (
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
password VARCHAR(255),
company_id INT(11) UNSIGNED NOT NULL,
valid INT(2) UNSIGNED NOT NULL,
dt DATETIME,
description VARCHAR(255),
PRIMARY KEY(id),
FOREIGN KEY(company_id) REFERENCES company(id));
-- product_category
CREATE TABLE IF NOT EXISTS product_category (
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
company_id INT(11) UNSIGNED NOT NULL,
valid INT(2) UNSIGNED NOT NULL,
dt DATETIME,
description VARCHAR(255),
PRIMARY KEY(id),
FOREIGN KEY(company_id) REFERENCES company(id));
-- product
CREATE TABLE IF NOT EXISTS product (
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
company_id INT(11) UNSIGNED NOT NULL,
amount INT(11) UNSIGNED NOT NULL,
valid INT(2) UNSIGNED NOT NULL,
dt DATETIME,
description VARCHAR(255),
PRIMARY KEY(id),
FOREIGN KEY(company_id) REFERENCES company(id));
-- product_category_value
CREATE TABLE IF NOT EXISTS product_category_value (
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
product_id INT(11) UNSIGNED NOT NULL,
product_category_id INT(11) UNSIGNED NOT NULL,
value VARCHAR(255),
valid INT(2) UNSIGNED NOT NULL,
dt DATETIME,
description VARCHAR(255),
PRIMARY KEY(id),
FOREIGN KEY(product_id) REFERENCES product(id),
FOREIGN KEY(product_category_id) REFERENCES product_category(id));
🖥️ Pages
로그인 화면
- 이메일과 패스워드를 이용하여 로그인
- 사용자가 입력한 패스워드와 DB에 저장되어있는 패스워드를 AES-256-ABC 복호화하여 비교
async decrypt_aes256abc (encryptedInput, key, iv) {
try {
let encKey = key ? key : this._password_secret_key;
let encIv = iv ? iv : Buffer.from(this._password_iv);
let cipher = crypto.createDecipheriv('aes-256-cbc', encKey, encIv);
let decipherText = cipher.update(encryptedInput, 'base64', 'utf8');
decipherText += cipher.final('utf8');
console.log(` : (Crypto.decrypt_aes256abc) plain text(${decipherText.length}) [${decipherText}]`);
return decipherText;
} catch (e) {
console.log(`\n : (Crypto.decrypt_aes256abc) Failed to decrypt \n`);
throw e;
}
};
- 로그인시 jwt를 이용하여 정보 저장
this._makeJwtToken = (dbItems) => {
try {
let tokenParams = {
user_id: dbItems.user[0].id,
company_id: dbItems.user[0].company_id
};
return jwt.sign(tokenParams, this._jwtSecretKey);
} catch (e) {
console.log(`\n : (Login._makeJwtToken) Failed to make jwt token \n`, e);
throw e;
}
};
회원가입 화면
- 이름, 이메일, 회사, 비밀번호를 입력하여 회원가입
- 이미 존재하는 이메일로는 가입 할 수 없음
let dbItems = await this._getUserDbItems(params);
if (this._isExistedEmail(dbItems)) {eCode.throwException(eCode().UserExistedEmail)};
- 비밀번호는 AES-256-ABC 암호화하여 저장
async encrypt_aes256cbc (plainText, key, iv) {
try {
let encKey = key ? key : this._password_secret_key;
let encIv = iv ? iv : Buffer.from(this._password_iv);
let cipher = crypto.createCipheriv('aes-256-cbc', encKey, encIv);
cipher.setAutoPadding(true);
let cipherText = cipher.update(Buffer.from(plainText), 'utf8', 'base64');
cipherText += cipher.final('base64');
console.log(` : (Crypto.encrypt_aes256cbc) cipher text(${cipherText.length}) [${cipherText}]`);
return cipherText;
} catch (e) {
console.log(`\n : (Crypto.encrypt_aes256cbc) Failed to encrypt \n`);
throw e;
}
};
- 입력한 회사가 이미 존재하면 존재하는 회사 ID로 사용자 추가, 없으면 새로운 회사 추가 뒤 사용자 추가
async addUser (dbInst, params) {
try {
let company = await executeSQL(dbInst, query.getCompanyByName(params));
if (company && company.length > 0) {
params['company_id'] = company[0].id;
} else {
await executeSQL(dbInst, query.addNewCompany(params));
let newCompany = await executeSQL(dbInst, query.getLastInsertId());
params['company_id'] = newCompany[0]['LAST_INSERT_ID()'];
}
await executeSQL(dbInst, query.addNewUser(params));
} catch (e) {
console.log(`\n : (RDS.addUser) Failed to add user : ${JSON.stringify(params)} \n`, e);
throw e;
}
};
대시보드
- 제품의 name(이름)과 amount(수량)는 필수 항목이나 그 외 카테고리는 사용자가 추가 할 수 있음
- 수량의 위, 아래 화살표로 수량을 쉽게 증가/감소 할 수 있음
- 제품 추가 시 여러 카테고리의 값도 추가 할 수 있게 배열 형태로 body 값 전달
categoryArr.forEach((item) => {
let id = `#input-${item.id}`;
let input = document.querySelector(id);
if (input.value.length > 0) {
values.push({
category_id: item.id,
value: input.value
});
}
});
let url = '/api/product';
let params = {
name: nameInput.value,
amount: amountInput.value,
values: values
};
await new Api().post(url, params);
async addProduct (dbInst, params) {
try {
await executeSQL(dbInst, query.addProduct(params));
let newProduct = await executeSQL(dbInst, query.getLastInsertId());
params['product_id'] = newProduct[0]['LAST_INSERT_ID()']
if (params.values) {
await Promise.all(params.values.map( async item => {
let productParams = {
product_id: params.product_id,
product_category_id: item.category_id,
value: item.value
};
await executeSQL(dbInst, query.addProductCategoryValue(productParams));
}));
}
} catch (e) {
console.log(`\n : (RDS.addProduct) Failed to add product \n`, e);
throw e;
}
};
- 하나의 제품에 여러 카테고리의 값이 있으며 프론트단에서 데이터 모양 변경을 최소화 하기 위하여 데이터를 정리하여 전달해줌
this._setCategoryValue = (result, item) => {
try {
let index = result.findIndex(element => {
return element.product_category_id === item.product_category_id;
});
if (index > -1) {
result[index].product_category_id = item.product_category_id;
result[index].product_category_name = item.product_category_name;
result[index].product_category_value = item.product_category_value_value;
}
return result;
} catch (e) {
console.log(`\n : (Product._setCategoryValue) Failed to set category value \n`, e);
throw e;
}
};
this._trimValueItems = (dbItems, item) => {
try {
let result = [];
dbItems.category.forEach(item => {
result.push({
product_category_id: item.id,
product_category_name: item.name,
product_category_value: ''
});
});
result = this._setCategoryValue(result, item);
return result;
} catch (e) {
console.log(`\n : (Product._trimValueItems) Failed to trim value items \n`, e);
throw e;
}
};
this._trimDbItems = (dbItems) => {
try {
let result = [];
dbItems.product.forEach(item => {
let index = result.findIndex(element => {
return element.product_id === item.product_id;
});
if (index > -1) {
this._setCategoryValue(result[index].product_values, item);
} else {
let product_values = this._trimValueItems(dbItems, item);
result.push({
product_id: item.product_id,
product_name: item.product_name,
product_amount: item.product_amount,
product_values: product_values
});
}
});
return result;
} catch (e) {
console.log(`\n : (Product._trimDbItems) Failed to trim DB items \n`, e);
throw e;
}
};
- 제품의 데이터를 변경 할 수 있음
프로필 화면
- 이메일, 현재 비밀번호, 새로운 비밀번호, 새로운 비밀번호 다시 입력으로 비밀번호를 변경 할 수 있음
- 로그인된 이메일과 입력한 비밀번호가 다르면 alert 발생
- login API를 이용하여 로그인에 실패하면 alert 발생
- 새로운 비밀번호와 다시 입력한 새로운 비밀번호가 다르면 alert 발생
async function changePassword (event) {
try {
event.preventDefault();
if (user[0].email === emailInput.value) {
await _login();
if (newPasswordInput.value === renewPasswordInput.value) {
await _changePassword();
alert(`정상적으로 비밀번호가 변경되었습니다. \n다시 로그인 해주십시오.`);
window.location.replace('/');
return ;
}
alert(`비밀번호가 다릅니다. \n확인 후 다시 시도해 주세요.`);
return ;
}
alert(`이메일이 다릅니다. \n확인 후 다시 시도해 주세요.`);
return ;
} catch (e) {
console.log(`\n : (Profile.changePassword) Failed to change password \n`, e);
alert(`문제가 발생하였습니다. 확인 후 다시 시도해 주세요. \n${new Base().getErrorMsg(e.message)}`);
throw e;
}
};
'Project' 카테고리의 다른 글
[@indeeeah/calculator] 패키지 배포 (0) | 2023.03.01 |
---|---|
[Lotto Check] 코드 설명 (0) | 2023.02.05 |
[Jelly Music Project] 코드 설명 (0) | 2023.02.05 |
[TA_IN Project] 코드 설명 (0) | 2023.02.05 |
재고 관리 시스템 시연 영상 (0) | 2023.02.01 |