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 |