Project

[Nice Inventory] 코드 설명

indeeah 2023. 2. 2. 20:45

간단한 인터페이스의 재고 관리 시스템입니다.

사용자가 가입할 때 등록한 회사별로 재고를 관리 할 수 있습니다.

간단한 인터페이스로 손쉽게 사용 할 수 있으며 쉽게 재고 및 정보를 관리 할 수 있습니다.

 

🌷 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`
  • `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;
    }
};