자바스크립트_Javascript

Javascript: 계산기 calculator

coding-abc.tistory.com 2025. 8. 13. 18:21
반응형

자바스크립트로 만든 계산기입니다.

아래와 같은 기능을 가지고 있습니다.

 

  • 사칙연산, 소수점, 퍼센트(일반 공학계산기 방식), 부호 전환(±)
  • 연속 계산(= 후 바로 이어서 연산 가능)
  • AC(전체 초기화), C(한 글자 삭제), 복사 버튼(결과 클립보드 복사)
  • 키보드 입력 지원: 숫자/+, -, *, /, ., =, Enter, Esc, Backspace, %

 

Javascript: 계산기 calculate

<!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