본문 바로가기

Work

재산세 계산기 JS code

Javascript는 좀 더 제대로 공부해야 하는 때까진 이번 토이 프로젝트로 마무리하자. 왜 상세값이 고지금액과 다르게 나왔는지는 여전히 모른다. 고지서의 과표기준액 숫자 자체가 이해 안 되니... 암튼 아래 내용은 기존에 알려진 재산세 계산 수식에 따른 것이다. 버그를 알게 되면 그 때 수정하겠음.

주) 대부분의 프로그래밍 언어가 그러하듯, 내용을 영어로 적다보니 출력 필드까지 모두 영어가 되었다. 이건 '쓸모있는 배포'를 하게 된다면 고치겠다. 지금은 어차피 아무도 안 볼 내용이므로.

calculator.html

<!DOCTYPE html>
<html lang="kr">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Calculator</title>
        <link href="styles.css" rel="stylesheet">
        <script src="script.js" defer></script>
    </head>
    <body>
        <p>Click "Add Property" button to add realestate property.</p>
        <p>IMPORTANT! No EMPTY PROPERTY item should remain. Refresh the screen to start again.</p>
        <p>MORE IMPORTANT!! "Add Property" will clear all preceding values in input forms.</p>
        <button id="btn-add-property" onclick="addProperty()">Add Property</button>
        <button id="btn-calculate" onclick="calculate()">Calculate</button>
        <button id="btn-show-debuginfo" onclick="showDOMInfo()">Show DOM</button>

        <table>
            <tr>
                <td class="view-cell" id="property-view">
                    <div class="item-list-header" id="property-list-header">
                        <label>Item #</label>
                        <label>Name (Optional)</label>
                        <label>Property Price(KRW)</label>
                        <label>Property Type</label>
                        <label>Property Area</label>
                        <label>Ownership Stake(%)</label>
                        <label>Utility Tax Exempted</label>
                    </div>
                </td>
                <td class="view-cell" id="output-view"></td>
                <td class="view-cell" id="debug-view"></td>
            </tr>
        </table>
    </body>
</html>

 

styles.css

*, *::before, *::after {
    box-sizing: border-box;
    font-family: Arial, Helvetica, sans-serif;
    font-weight: normal;
}

body {
    padding: 0;
    margin: 0;
    background: linear-gradient(to right, #f7ff8b, #9dcdf3);
}

.item-list-header {
    display: grid;
    grid-template-columns: repeat(7, 135px);
    grid-template-rows: 40px;
    margin: 5px;
    /*border: 1px solid lightgrey;*/
}

.item-list-header > label {
    cursor: help;
    font-size: 1rem;
    color: rgba(0, 50, 100);
    align-self: center;
    text-align-last: left;
    margin: 5px;
    width: 125px;
    /*border: 1px solid lightgrey;*/
}

.item-field {
    display: grid;
    grid-template-columns: repeat(7, 135px);
    grid-template-rows: 40px;
    margin: 5px;
    border: 1px solid lightgrey;
}

.item-field > label {
    cursor: help;
    font-size: 1rem;
    align-self: center;
    text-align-last: left;
    margin: 5px;
    width: 125px;
    /*border: 1px solid lightgrey;*/
}

.item-field > input {
    cursor: text;
    font-size: 1.2rem;
    align-self: center;
    border: 1px solid white;
    background-color: rgba(255, 255, 255, .65);
    margin: 5px;
    width: 125px;
}

.item-field > input:hover {
    background-color: rgba(255, 255, 255, .9);
}

.item-field > input:focus {
    background-color: rgba(240, 240, 255, .9);
}

.item-field > select {
    cursor: pointer;
    font-size: 1rem;
    align-self: center;
    border: 1px solid white;
    background-color: rgba(255, 255, 255, .65);
    margin: 5px;
    width: 125px;
}

.item-field > select:hover {
    background-color: rgba(255, 255, 255, .9);
}

.item-input-checkbox:hover{
    cursor: pointer;
    border: 1px solid lightgrey;
}

.view-cell{
    vertical-align: top;
    min-width: 350px;
    padding: 20px;
}

 

script.js

class Property {
    constructor(name="", price=0, type="", area="", stake=0) {
        this.name = name; ///optional, shall be 'Property {N}' unless specified
        this.price = price; ///positive integer
        this.type = type; ///Residential, Secondary
        this.area = area; ///Normal, Supervision, Speculation
        this.stake = stake; ///0~100 in percentile integer
        this.isUtilityTaxExempted = false;
    }
}

function showDOMInfo() {
    clear(debugView)
    print(debugView, "[Debug Info]<br>item-field count: " + items.length);
    for (let i=0; i<items.length; i++) {
        printline(debugView, items[i].className + i + " child count: " + items[i].childElementCount);
        if (items[i].className === "item-field") {
            //print(debugView, "item-field #" + i + " childNode length: " + items[i].childNodes.length + "<br>");
            // for (n=0; n<items[i].childNodes.length; n++) {
            //     print(debugView, "childNode" + n + ": " + items[i].childNodes[n] + items[i].childNodes[n].textContent + "<br>");
            // }
            for (let n=0; n<items[i].children.length; n++) {
                printline(debugView, "__child" + n + " content: " + items[i].children[n] + items[i].children[n].textContent);
                printline(debugView, "__child" + n + " nodeName: " + items[i].children[n].nodeName);
                printline(debugView, "__child" + n + " value: " + items[i].children[n].value);
            }
        }
    }
}

function addProperty(price = 0) {
    let newPropertyId = items.length + 1;
    print(propertyView, '<div class="item-field">\
                            <label class="item-label">Property ' + newPropertyId + ':</label>\
                            <input class="item-input-name">\
                            <input class="item-input-price" value="' + price + '">\
                            <select class="item-input-type" id="propertyType">\
                                <option>Residential</option>\
                                <option>Secondary</option>\
                            </select>\
                            <select class="item-input-area" id="propertyArea">\
                                <option>Normal</option>\
                                <option>Supervision</option>\
                                <option>Speculation</option>\
                            </select>\
                            <input class="item-input-stake">\
                            <input utility-tax-exemption type="checkbox" class="item-input-checkbox">\
                        </div>');
    //document.getElementById("property-list-header").hidden = false;
    document.getElementById("btn-calculate").disabled = false; /// we need property at lease one to calculate.
}

function buildPropertyList(items) {
    printline(outputView, "Total property count: " + items.length);
    let propertyList = [];
    for (p=0; p<items.length; p++) {
        printline(debugView, "items[" + p + "] field count: " + items[p].children.length);
        let property = new Property();
        for (f=0; f<items[p].children.length; f++) {
            print(debugView, "field[" + f + "]: " + items[p].children[f].className);
            printline(debugView, " --> value: " + items[p].children[f].value);
            if (items[p].children[f].className === "item-input-name") {
                property.name = items[p].children[f].value;
                if (property.name === '') property.name = "Property " + (p+1);
            } else if (items[p].children[f].className === "item-input-price") {
                property.price = Math.max(0, parseInt(items[p].children[f].value));
                if (isNaN(property.price)) property.price = -1; /// this item shall be excluded from calculation.
            } else if (items[p].children[f].className === "item-input-type") {
                property.type = items[p].children[f].value;
            } else if (items[p].children[f].className === "item-input-area") {
                property.area = items[p].children[f].value;
            } else if (items[p].children[f].className === "item-input-stake") {
                property.stake = saturate(parseFloat(items[p].children[f].value), 0, 100);
                if (isNaN(property.stake)) property.stake = -1; /// this item shall be excluded from calculation.
            } else if (items[p].children[f].className === "item-input-checkbox") {
                property.isUtilityTaxExempted = items[p].children[f].checked;
                printline(debugView, "isUtilityTaxExempted: " + property.isUtilityTaxExempted);
            } else {
                //WARNING("Invalid data field detected: " + items[p].children[f].textContent) /// we don't WARNING for decoration fields(such as label).
            }
        }
        printline(debugView, "item "+ p + " loop finished:" + f)
        propertyList.push(property);
    }

    for (property of propertyList) {
        printline(debugView, property.name);
        printline(debugView, "price: " + property.price);
        printline(debugView, "type: " + property.type);
        printline(debugView, "area: " + property.area);
        printline(debugView, "stake: " + property.stake);
    }
    return propertyList;
}

function calculate() {
    clear(debugView)
    clear(outputView)

    //1. read property price (offcial)
    let propertyList = buildPropertyList(items);

    // propertyList.forEach(property => {
    //     console.log(property);
    //     printline(debugView, property.name);
    // })

    for (property of propertyList) {
        //small check on validity
        if (!isValidPropertyInfo(property)) {
            printline(outputView, "Skipping: Invalid property info in " + property.name);
            continue;
        }

        //title of output report
        printline(outputView, "<br>=== " + property.name + " ===");

        //derate price over deduction rate(60%)
        printline(outputView, "Property Price Deducted: " + currencyIntToStr(getPriceDeducted(property)));

        //2. get basic tax
        let propertyBasicTax = getBasicTax(property);
        printline(outputView, "Property Basic Tax: " + currencyIntToStr(propertyBasicTax));

        //3. estimate city tax
        let propertyCityTax = getCityTax(property);
        printline(outputView, "Property City Tax: " + currencyIntToStr(propertyCityTax));

        //sum of property tax
        printline(outputView, "① Property Tax Sum: " + currencyIntToStr((propertyBasicTax + propertyCityTax)));

        //4. get public utility tax
        let publicUtilityTax = 0;
        if (property.isUtilityTaxExempted === true) {
            printline(outputView, "② Public Utility Tax Exempted");
        }
        else {
            publicUtilityTax = getPublicUtilityTax(property);
            printline(outputView, "② Public Utility Tax: " + currencyIntToStr(publicUtilityTax));
        }

        //5. estimate local education tax from basic tax * local education tax rate(20%)
        let localEducationTax = getLocalEducationTax(propertyBasicTax);
        printline(outputView, "③ Local Education Tax: " + currencyIntToStr(localEducationTax));

        let netPropertyTax = propertyBasicTax + propertyCityTax + publicUtilityTax + localEducationTax;        
        printline(outputView, "Net Property Tax: " + currencyIntToStr(netPropertyTax));

        let netPropertyTaxPortion = netPropertyTax * property.stake / 100;
        printline(outputView, "→ At stake portion: " + currencyIntToStr(netPropertyTaxPortion));
        if (netPropertyTaxPortion > 100000) ///연납 기준
            printline(outputView, "→ Payment: " + currencyIntToStr(netPropertyTaxPortion / 2));
        else
            printline(outputView, "→ Payment: " + currencyIntToStr(netPropertyTaxPortion));
    }
}
function getPriceDeducted(property) {
    const PROPERTY_PRICE_DEDUCTION_RATE = 0.6;
    return property.price * PROPERTY_PRICE_DEDUCTION_RATE;
}

function getBasicTax(property) {
    const TAX_RATE_SECONDARY = [-1, 0.4, 0];
    const TAX_TABLE_RESIDENTIAL = [
        [60000000, 0.001, 0],
        [150000000, 0.0015, 60000],
        [300000000, 0.0025, 195000],
        [-1, 0.004, 570000]];
    const TABLE_IDX_LEVEL_REF = 0;
    const TABLE_IDX_RATE = 1;
    const TABLE_IDX_OFFSET = 2;

    let taxTable, tIndex;
    /// select rate table
    if (property.type === "Secondary") {
        taxTable = TAX_RATE_SECONDARY;
    } else if (property.type === "Residential") {
        taxTable = TAX_TABLE_RESIDENTIAL;
    } else {
        WARNING("Invalid property type deteced: " + property.type)
        return -1;
    }

    /// find tax rate and offset from the table
    let propertyPriceDeducted = getPriceDeducted(property);
    for (tIndex=0; tIndex<taxTable.length; tIndex++) {
        if (propertyPriceDeducted <= taxTable[tIndex][TABLE_IDX_LEVEL_REF] || taxTable[tIndex][TABLE_IDX_LEVEL_REF] === -1) break;
    }
    let propertyBasicTaxLevelRef;
    if (tIndex === 0) propertyBasicTaxLevelRef = 0;
    else propertyBasicTaxLevelRef = taxTable[tIndex-1][TABLE_IDX_LEVEL_REF];
    let propertyBasicTaxRate = taxTable[tIndex][TABLE_IDX_RATE];
    let propertyBasicTaxOffset = taxTable[tIndex][TABLE_IDX_OFFSET];
    printline(debugView, "[property tax] deducted price: " + propertyPriceDeducted + " level ref: " + propertyBasicTaxLevelRef)
    printline(debugView, "[property tax] tax Rate: " + propertyBasicTaxRate + ", tax Offset: " + propertyBasicTaxOffset);
 
    // estimate basic tax from (derated price - basic tax level ref) * basic tax rate + basic tax offset
    let propertyBasicTax = (propertyPriceDeducted - propertyBasicTaxLevelRef) * propertyBasicTaxRate + propertyBasicTaxOffset;
    printline(debugView, "propertyPriceDeducted: " + propertyPriceDeducted);
    printline(debugView, "propertyBasicTaxLevelRef: " + propertyBasicTaxLevelRef);
    printline(debugView, "propertyBasicTaxRate: " + propertyBasicTaxRate);
    printline(debugView, "propertyBasicTaxOffset: " + propertyBasicTaxOffset);
    printline(debugView, "propertyBasicTax: " + propertyBasicTax);
    return propertyBasicTax;
}

function getCityTax(property) {
    const PROPERTY_CITY_TAX_RATE = 0.0014;
    printline(debugView, "[city tax] tax Rate: " + PROPERTY_CITY_TAX_RATE);
    return getPriceDeducted(property) * PROPERTY_CITY_TAX_RATE;
}

function getPublicUtilityTax(property) {
    const PUBLIC_UTILITY_TAX_TABLE = [
        [6000000, 0.0004, 0],
        [13000000, 0.0005, 2400],
        [26000000, 0.0006, 5900],
        [39000000, 0.0008, 13700],
        [64000000, 0.001, 24100],
        [-1, 0.0012, 49100]];
    const TABLE_IDX_LEVEL_REF = 0;
    const TABLE_IDX_RATE = 1;
    const TABLE_IDX_OFFSET = 2;
    let taxTable = PUBLIC_UTILITY_TAX_TABLE, tIndex;
    let propertyPriceDeducted = getPriceDeducted(property);
    for (tIndex=0; tIndex<taxTable.length; tIndex++) {
        if (propertyPriceDeducted <= taxTable[tIndex][TABLE_IDX_LEVEL_REF] || taxTable[tIndex][TABLE_IDX_LEVEL_REF] === -1) break;
    }    
    let utilityTaxLevelRef;
    if (tIndex === 0) utilityTaxLevelRef = 0;
    else utilityTaxLevelRef = taxTable[tIndex-1][TABLE_IDX_LEVEL_REF];
    let utilityTaxRate = taxTable[tIndex][TABLE_IDX_RATE];
    let utilityTaxOffset = taxTable[tIndex][TABLE_IDX_OFFSET];
    printline(debugView, "[utility tax] deducted price: " + propertyPriceDeducted + " level ref: " + utilityTaxLevelRef)
    printline(debugView, "[utility tax] tax Rate: " + utilityTaxRate + ", tax Offset: " + utilityTaxOffset);

    // estimate basic tax from (derated price - basic tax level ref) * basic tax rate + basic tax offset
    let publicUtilityTax = (propertyPriceDeducted - utilityTaxLevelRef) * utilityTaxRate + utilityTaxOffset;
    return publicUtilityTax;
}

function getLocalEducationTax(propertyBasicTax) {
    const LOCAL_EDUCATION_TAX_RATE = 0.2;
    printline(debugView, "[local education tax] tax Rate: " + LOCAL_EDUCATION_TAX_RATE);
    return propertyBasicTax * LOCAL_EDUCATION_TAX_RATE;
}

function isValidPropertyInfo(property) {
    if (property.price <= 0 || property.stake <= 0) {
        return false;
    } else {
        return true;
    }
}

function numberStrToInt(numberStr) {
    //print(debugView, "input: " + numberStr + "(" + numberStr.type + ")");
    numberStr = numberStr.replaceAll(',', '');
    const numberInt = parseInt(numberStr);
    //printline(debugView,  " -> input refined: " + numberInt + "(" + numberInt.type + ")");
    return numberInt;
}

function getCurrencySymbol(currencyCode="kr") {
    switch (currencyCode) {
        case "kr":
            return "₩";
        case "us":
            return "$";
        case "eu":
            return "€";
        case "cn":
        case "jp":
            return "¥";
        case "uk":
            return "£";
    }
}
function currencyIntToStr(numberInt, currencyCode="kr") {
    let numberStr = numberInt.toLocaleString('en', {maximumFractionDigits : 0});
    return getCurrencySymbol("kr") + numberStr;
}

function print(view, text, linefeed=false) {
    view.innerHTML += text;
    if (linefeed === true) {
        view.innerHTML += "<br>"
    }
}

function printline(view, text) {
    print(view, text, true);
}

function WARNING(view, text) {
    printline(outputView, "[WARN]" + text); ///WARNING info should show up on main view
}

function clear(view) {
    view.innerHTML = "";
}

function saturate(val, min, max) {
    return Math.max(0, Math.min(100, val));
}

String.prototype.replaceAll = function(org, dest) {
    return this.split(org).join(dest);
}
const items = document.getElementsByClassName("item-field");
const propertyView = document.getElementById("property-view");
const outputView = document.getElementById("output-view");
const debugView = document.getElementById("debug-view");
debugView.hidden = false;

document.getElementById("btn-calculate").disabled = true;
document.getElementById("btn-show-debuginfo").disabled = false;
document.getElementById("property-list-header").hidden = true; ///TO-DO: this doesn't work!
addProperty(228000000);

 

Visual Studio Code에서 Javascript 디버깅을 할 줄 모르니 정말 멍청하게 작업해야 했다. Thor의 망치도 모르는 이에겐 그냥 망치쇠기둥 막대일 뿐이겠지.

'Work' 카테고리의 다른 글

프로그래밍 언어 이력  (0) 2020.07.30
Reprogramming  (0) 2020.07.30
빅 워크를 위한 자산 조사  (0) 2020.07.15
사내 공모  (0) 2020.07.07
Python - ndarray 배열 1차원화  (0) 2020.07.06