반응형
자바스크립트로 만든 계산기입니다.
아래와 같은 기능을 가지고 있습니다.
- 사칙연산, 소수점, 퍼센트(일반 공학계산기 방식), 부호 전환(±)
- 연속 계산(= 후 바로 이어서 연산 가능)
- AC(전체 초기화), C(한 글자 삭제), 복사 버튼(결과 클립보드 복사)
- 키보드 입력 지원: 숫자/+, -, *, /, ., =, Enter, Esc, Backspace, %
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>JavaScript Calculator</title>
<style>
:root{
--bg:#0f1220;--panel:#171b2e;--btn:#212642;--btn-accent:#5865f2;--text:#e8ecff;--muted:#9aa3c7;--danger:#ff6b6b;--ok:#2ecc71
}
*{box-sizing:border-box}
body{margin:0;min-height:100svh;display:grid;place-items:center;background:radial-gradient(60% 80% at 50% 20%, #1a1f3b 0, #0f1220 60%);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,Apple Color Emoji,Segoe UI Emoji}
.calc{width:min(420px,92vw);background:var(--panel);border-radius:24px;box-shadow:0 20px 50px rgba(0,0,0,.45);padding:18px}
.top{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.brand{color:var(--muted);font-size:.85rem;letter-spacing:.12em;text-transform:uppercase}
.screen{background:linear-gradient(180deg,#1e2441,#1a2040);color:var(--text);border-radius:16px;padding:14px 16px;text-align:right;min-height:88px;box-shadow:inset 0 0 0 1px rgba(255,255,255,.06)}
.screen .history{font-size:.9rem;color:var(--muted);min-height:1.3em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.screen .current{font-size:2.4rem;line-height:1.2;font-variant-numeric:tabular-nums}
.keys{display:grid;gap:10px;margin-top:16px;grid-template-columns:repeat(4,1fr)}
button{appearance:none;border:none;background:var(--btn);color:var(--text);padding:18px 14px;border-radius:16px;font-size:1.2rem;font-weight:600;cursor:pointer;box-shadow:0 6px 14px rgba(0,0,0,.35);transition:transform .04s ease,filter .2s ease,box-shadow .2s ease}
button:active{transform:translateY(1px)}
button.op{background:#26305e}
button.equal{background:var(--btn-accent);}
button.wide{grid-column:span 2}
button.danger{background:#4a1f2a}
button.secondary{background:#24304a;color:#c7d1ff}
button:focus-visible{outline:2px solid #8ea0ff;outline-offset:2px}
.footer{margin-top:10px;text-align:center;color:var(--muted);font-size:.8rem}
.kbd{display:inline-grid;grid-auto-flow:column;gap:2px;background:#0e1222;border-radius:8px;padding:2px 6px;border:1px solid rgba(255,255,255,.06)}
.kbd kbd{font-family:ui-monospace,Consolas,Menlo,monospace;font-size:.8em}
</style>
</head>
<body>
<main class="calc" role="application" aria-label="계산기">
<div class="top">
<div class="brand">JS Calculator</div>
<div class="brand" id="status" aria-live="polite"></div>
</div>
<div class="screen" aria-live="polite" aria-atomic="true">
<div class="history" id="history"> </div>
<div class="current" id="current">0</div>
</div>
<div class="keys" role="group" aria-label="키패드">
<button class="secondary" data-action="clear-all" aria-label="모두 지움 (Esc)">AC</button>
<button class="secondary" data-action="clear-entry" aria-label="한 글자 지움 (Backspace)">C</button>
<button class="secondary" data-action="percent" aria-label="퍼센트">%</button>
<button class="op" data-op="/" aria-label="나눗셈 (/)">÷</button>
<button data-num="7">7</button>
<button data-num="8">8</button>
<button data-num="9">9</button>
<button class="op" data-op="*" aria-label="곱셈 (*)">×</button>
<button data-num="4">4</button>
<button data-num="5">5</button>
<button data-num="6">6</button>
<button class="op" data-op="-" aria-label="뺄셈 (-)">−</button>
<button data-num="1">1</button>
<button data-num="2">2</button>
<button data-num="3">3</button>
<button class="op" data-op="+" aria-label="덧셈 (+)">+</button>
<button class="wide" data-num="0">0</button>
<button data-action="dot">.</button>
<button class="equal" data-action="equals" aria-label="결과 (=)">=</button>
<button class="danger wide" data-action="toggle-sign" aria-label="부호 전환 (±)">±</button>
<button class="secondary wide" data-action="copy" aria-label="복사 (Ctrl+C)">Copy</button>
</div>
<div class="footer">
키보드: <span class="kbd"><kbd>0–9</kbd> <kbd>+</kbd> <kbd>-</kbd> <kbd>*</kbd> <kbd>/</kbd> <kbd>.</kbd> <kbd>=</kbd> <kbd>Enter</kbd> <kbd>Esc</kbd> <kbd>Backspace</kbd></span>
</div>
</main>
<script>
(function(){
const currentEl = document.getElementById('current');
const historyEl = document.getElementById('history');
const statusEl = document.getElementById('status');
/** Calculator state */
let a = null; // first operand
let op = null; // operator: '+', '-', '*', '/'
let b = null; // second operand
let entering = 'a'; // which operand we are typing: 'a' | 'b'
let justEvaluated = false;
const fmt = (n) => {
// avoid scientific notation for typical range, limit decimals
if (n === null || n === undefined || Number.isNaN(n)) return 'NaN';
if (!Number.isFinite(n)) return n > 0 ? '∞' : '−∞';
const s = n.toString();
if (/e/i.test(s)) return n.toLocaleString(undefined,{maximumFractionDigits:12});
// trim trailing zeros
let [int, dec] = s.split('.');
if (!dec) return Number(int).toLocaleString();
dec = dec.replace(/0+$/, '');
const joined = dec ? int + '.' + dec : int;
// add thousands separators only to int part
const parts = joined.split('.');
parts[0] = Number(parts[0]).toLocaleString();
return parts.join('.');
};
const setCurrent = (text) => (currentEl.textContent = text);
const setHistory = (text) => (historyEl.textContent = text || '\u00A0');
const setStatus = (text) => (statusEl.textContent = text || '');
const clearAll = () => {
a = b = null; op = null; entering = 'a'; justEvaluated = false; setCurrent('0'); setHistory(''); setStatus('');
};
const clearEntry = () => {
if (entering === 'a') { a = null; setCurrent('0'); }
else { b = null; setCurrent('0'); }
};
const inputDigit = (d) => {
if (justEvaluated && entering === 'a') { // start new calc
clearAll();
}
if (entering === 'a') {
a = buildNumber(a, d);
setCurrent(fmt(a ?? 0));
} else {
b = buildNumber(b, d);
setCurrent(fmt(b ?? 0));
}
};
const buildNumber = (cur, d) => {
const s = cur == null ? '' : String(cur);
// if s already includes '.', preserve as string while editing
if (s.includes('.') || buffer.includes('.')) {
buffer = (buffer === '0' ? '' : buffer) + d;
const n = Number(buffer);
return Number.isNaN(n) ? 0 : n;
}
const n = (cur == null ? 0 : cur) * 10 + d;
return n;
};
let buffer = '0';
const syncBufferFrom = (val) => { buffer = (val==null? '0' : String(val)); };
const inputDot = () => {
if (justEvaluated && entering === 'a') { clearAll(); }
if (!buffer.includes('.')) buffer += '.';
const val = Number(buffer);
if (entering === 'a') a = Number.isNaN(val) ? a : val; else b = Number.isNaN(val) ? b : val;
setCurrent(buffer);
};
const setOperator = (nextOp) => {
if (a == null) a = 0;
if (op && b != null) {
// chain operations
const res = evaluate(a, op, b);
a = res; b = null; setCurrent(fmt(a)); setHistory(`${fmt(a)} ${symbol(op)}`);
}
op = nextOp; entering = 'b'; syncBufferFrom(b); setHistory(`${fmt(a)} ${symbol(op)}`); justEvaluated = false;
};
const toggleSign = () => {
if (entering === 'a') { a = (a??0) * -1; syncBufferFrom(a); setCurrent(fmt(a)); }
else { b = (b??0) * -1; syncBufferFrom(b); setCurrent(fmt(b)); }
};
const percent = () => {
// Common calculator behavior: b becomes a * (b/100) when op is set; otherwise a = a/100
if (op && entering === 'b') {
b = (b ?? a ?? 0) / 100 * (a ?? 0);
syncBufferFrom(b); setCurrent(fmt(b));
} else {
a = (a ?? 0) / 100; syncBufferFrom(a); setCurrent(fmt(a));
}
};
const equals = () => {
if (op == null) { setStatus('연산자가 없습니다'); return; }
if (b == null) { b = a ?? 0; }
const res = evaluate(a ?? 0, op, b ?? 0);
setHistory(`${fmt(a)} ${symbol(op)} ${fmt(b)} =`);
a = res; setCurrent(fmt(a)); op = null; entering = 'a'; b = null; justEvaluated = true; syncBufferFrom(a);
};
const backspace = () => {
if (!buffer || buffer === '0') return;
buffer = buffer.slice(0, -1);
if (buffer === '' || buffer === '-' || buffer === '-0') buffer = '0';
const val = Number(buffer);
if (entering === 'a') a = Number.isNaN(val) ? a : val; else b = Number.isNaN(val) ? b : val;
setCurrent(buffer);
};
const copyToClipboard = async () => {
try { await navigator.clipboard.writeText(currentEl.textContent.replace(/,/g,'')); setStatus('복사됨'); setTimeout(()=>setStatus(''),1200);} catch { setStatus('복사 실패'); }
};
function evaluate(x, operator, y){
x = Number(x); y = Number(y);
switch(operator){
case '+': return x + y;
case '-': return x - y;
case '*': return x * y;
case '/': return y === 0 ? (x === 0 ? NaN : (x>0? Infinity : -Infinity)) : x / y;
default: return x;
}
}
const symbol = (o) => ({'/':'÷','*':'×','+':'+','-':'−'}[o]||o);
// Event delegation for clicks
document.querySelector('.keys').addEventListener('click', (e)=>{
const btn = e.target.closest('button'); if(!btn) return;
const num = btn.getAttribute('data-num');
const opAttr = btn.getAttribute('data-op');
const action = btn.getAttribute('data-action');
if (num !== null){
if (buffer === '0' && !buffer.includes('.')) buffer = '';
inputDigit(Number(num));
} else if (opAttr){
entering = 'b'; if (buffer === '0') syncBufferFrom(b);
setOperator(opAttr);
} else if (action){
switch(action){
case 'dot': inputDot(); break;
case 'equals': equals(); break;
case 'clear-all': clearAll(); break;
case 'clear-entry': backspace(); break;
case 'percent': percent(); break;
case 'toggle-sign': toggleSign(); break;
case 'copy': copyToClipboard(); break;
}
}
});
// Keyboard support
window.addEventListener('keydown', (e)=>{
const k = e.key;
if (/^[0-9]$/.test(k)) { if (buffer === '0' && !buffer.includes('.')) buffer=''; inputDigit(Number(k)); }
else if (k === '.') { inputDot(); }
else if (k === '+' || k === '-' || k === '*' || k === '/') { setOperator(k); }
else if (k === '=' || k === 'Enter') { e.preventDefault(); equals(); }
else if (k === 'Escape') { clearAll(); }
else if (k === 'Backspace') { backspace(); }
else if (k === '%') { percent(); }
});
// initialize
clearAll();
})();
</script>
</body>
</html>
반응형
'자바스크립트_Javascript' 카테고리의 다른 글
Javascript: 조건문 if (4) | 2025.08.15 |
---|---|
Javascript: 함수 Function (11) | 2025.08.14 |
Javascript: 연산자 Operators (4) | 2025.08.13 |
Javascript: 호이스팅 Hoisting (1) | 2025.08.13 |
Javascript: 변수 Variable var let const (5) | 2025.08.12 |