๐Ÿฏ[TIL] 250718-034

byoยท2025๋…„ 7์›” 21์ผ

๐Ÿ’ซ JAVA

๐Ÿงฎ Calculator with Js and Thymeleaf

๐Ÿ“ก controller

@Controller
public class CalculatorController {

    @GetMapping("/")
    public String index() {
        // (src/main/resources/templates/index.html ์„ ์˜๋ฏธ, Thymeleaf ์‚ฌ์šฉ ์‹œ)
        return "index";
    }

    @PostMapping("/calculate")
    @ResponseBody // ๊ฒฐ๊ณผ๋ฅผ JSON ํ˜•์‹์œผ๋กœ ๋ธŒ๋ผ์šฐ์ €์— ์ง์ ‘ ์‘๋‹ตํ•œ๋‹ค.
    public Map<String, Double> calculate(
            // num1, num2, operator๋ฅผ ์š”์ฒญ์—์„œ ๋ฐ›์•„์˜จ๋‹ค.
            @RequestParam double num1,
            @RequestParam double num2,
            @RequestParam String operator
    ) {
        double result = switch (operator) {
            case "add" -> num1 + num2;
            case "subtract" -> num1 - num2;
            case "multiply" -> num1 * num2;
            case "divide" -> num2 != 0 ? num1 / num2 : Double.NaN;
            default -> Double.NaN;
        };

        // ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ํ‚ค "result"๋กœ ๊ฐ–๋Š” Map ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ (JSON ํ˜•์‹์œผ๋กœ ์ž๋™ ๋ณ€ํ™˜)
        return Map.of("result", result);
    }
}

๐Ÿ–ผ๏ธ thymeleaf

<body>
<div class="calculator-container">
    <div id="display">0</div>
    <div class="calculator-grid">
        <button class="btn function" data-action="clear">C</button>
        <button class="btn function" data-action="divide">รท</button>
        <button class="btn function" data-action="multiply">ร—</button>
        <button class="btn function" data-action="subtract">โˆ’</button>

        <button class="btn digit" data-digit="7">7</button>
        <button class="btn digit" data-digit="8">8</button>
        <button class="btn digit" data-digit="9">9</button>
        <button class="btn function" data-action="add">+</button>

        <button class="btn digit" data-digit="4">4</button>
        <button class="btn digit" data-digit="5">5</button>
        <button class="btn digit" data-digit="6">6</button>
        <button class="btn" data-action="equals" rowspan="2">=</button>

        <button class="btn digit" data-digit="1">1</button>
        <button class="btn digit" data-digit="2">2</button>
        <button class="btn digit" data-digit="3">3</button>

        <button class="btn digit zero" data-digit="0">0</button>
        <button class="btn digit" data-digit=".">.</button>
    </div>
</div>
<script th:src="@{/main.js}"></script>
</body>

๐ŸŽฏ js

const display = document.getElementById('display');

let first = '';          // ์ฒซ ๋ฒˆ์งธ ์ˆซ์ž
let second = '';         // ๋‘ ๋ฒˆ์งธ ์ˆซ์ž
let operator = null;     // ์„ ํƒํ•œ ์—ฐ์‚ฐ์ž(+, -, *, /)
let resultDisplayed = false;  // ๊ฒฐ๊ณผ๊ฐ€ ํ™”๋ฉด์— ํ‘œ์‹œ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€

// ์ˆซ์ž ๋ฒ„ํŠผ์— ํด๋ฆญ ์ด๋ฒคํŠธ ์ถ”๊ฐ€
document.querySelectorAll('.digit').forEach(
    btn => {
        btn.addEventListener('click', () => {
            // ๊ฒฐ๊ณผ๊ฐ€ ์ด๋ฏธ ํ‘œ์‹œ๋œ ์ƒํƒœ๋ผ๋ฉด, ์ƒˆ ์ˆซ์ž ์ž…๋ ฅ์„ ์œ„ํ•ด ์ฒซ ๋ฒˆ์งธ ์ˆซ์ž ์ดˆ๊ธฐํ™”
            if (resultDisplayed) {
                first = '';
                resultDisplayed = false;
            }

            // ํด๋ฆญํ•œ ๋ฒ„ํŠผ์˜ data-digit ์†์„ฑ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ
            const digit = btn.dataset.digit;  

            if (!operator) {  // ์—ฐ์‚ฐ์ž๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์€ ์ƒํƒœ(์ฒซ ๋ฒˆ์งธ ์ˆซ์ž ์ž…๋ ฅ ์ค‘)
                if (!(digit === '.' && first.includes('.'))) first += digit; // ์†Œ์ˆ˜์ (.)์ด ์ค‘๋ณต ์ž…๋ ฅ๋˜์ง€ ์•Š๋„๋ก ์กฐ๊ฑด ํ™•์ธ ํ›„ ์ˆซ์ž ์ถ”๊ฐ€
                display.textContent = first || '0'; // ํ™”๋ฉด์— ์ฒซ ๋ฒˆ์งธ ์ˆซ์ž ํ‘œ์‹œ, ๊ฐ’์ด ์—†์œผ๋ฉด 0 ํ‘œ์‹œ
                display.scrollLeft = display.scrollWidth;  // ์ˆซ์ž๊ฐ€ ๊ธธ์–ด์ง€๋ฉด ์˜ค๋ฅธ์ชฝ ๋์œผ๋กœ ์Šคํฌ๋กค ์ด๋™
            } else {  // ์—ฐ์‚ฐ์ž๊ฐ€ ์„ ํƒ๋œ ์ƒํƒœ(๋‘ ๋ฒˆ์งธ ์ˆซ์ž ์ž…๋ ฅ ์ค‘)
                if (!(digit === '.' && second.includes('.'))) second += digit; // ์†Œ์ˆ˜์ (.) ์ค‘๋ณต ์ž…๋ ฅ ๋ฐฉ์ง€ ํ›„ ๋‘ ๋ฒˆ์งธ ์ˆซ์ž ์ถ”๊ฐ€
                display.textContent = second || '0'; // ํ™”๋ฉด์— ๋‘ ๋ฒˆ์งธ ์ˆซ์ž ํ‘œ์‹œ, ๊ฐ’ ์—†์œผ๋ฉด 0 ํ‘œ์‹œ
                display.scrollLeft = display.scrollWidth; // ์˜ค๋ฅธ์ชฝ ๋์œผ๋กœ ์Šคํฌ๋กค ์ด๋™
            }
        });
    }
)

// ๊ธฐ๋Šฅ ๋ฒ„ํŠผ(ํด๋ž˜์Šค 'function') ๋ชจ๋‘์— ํด๋ฆญ ์ด๋ฒคํŠธ ์ถ”๊ฐ€
document.querySelectorAll('.function').forEach(
    btn => {
        btn.addEventListener('click', () => {
            const action = btn.dataset.action;  // ํด๋ฆญํ•œ ๋ฒ„ํŠผ์˜ data-action ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ

            if (action === 'clear') {  // 'clear' ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ
                first = '';            // ์ฒซ ๋ฒˆ์งธ ์ˆซ์ž ์ดˆ๊ธฐํ™”
                second = '';           // ๋‘ ๋ฒˆ์งธ ์ˆซ์ž ์ดˆ๊ธฐํ™”
                operator = null;       // ์—ฐ์‚ฐ์ž ์ดˆ๊ธฐํ™”
                display.textContent = '0';  // ํ™”๋ฉด์— 0 ํ‘œ์‹œ
                return;                // ํ•จ์ˆ˜ ์ข…๋ฃŒ
            }

            if (action === 'equals') return;  // '=' ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์—ฌ๊ธฐ์„œ๋Š” ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ  ์•„๋ž˜ ๋ณ„๋„ ์ด๋ฒคํŠธ๋กœ ์ด๋™

            operator = action;     // ์—ฐ์‚ฐ์ž ์ €์žฅ (add, subtract, multiply, divide ์ค‘ ํ•˜๋‚˜)
            resultDisplayed = false;  // ๊ฒฐ๊ณผ ํ‘œ์‹œ ์ƒํƒœ ์ดˆ๊ธฐํ™”
        })
    }
)

// '=' ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ ๋“ฑ๋ก
document.querySelector('[data-action="equals"]').addEventListener(
    'click',
    () => {
        // ์„œ๋ฒ„์— ๋ณด๋‚ผ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ key=value ํ˜•์‹์œผ๋กœ ์ƒ์„ฑ
        const params = new URLSearchParams({
            num1: first,
            num2: second,
            operator
        });
        // fetch API๋กœ POST ์š”์ฒญ ๋ณด๋‚ด๊ธฐ
        fetch('/calculate', {
            method: 'POST',  // POST
            headers: { 'Content-Type': 'application/x-www-form-urlencoded'},  // ํผ ๋ฐ์ดํ„ฐ ํ˜•์‹ ์ง€์ •
            body: params  // ๋ณด๋‚ผ ๋ฐ์ดํ„ฐ
        }).then(res => res.json())  // ์‘๋‹ต์„ JSON์œผ๋กœ ๋ณ€ํ™˜
        .then(data => {
            display.textContent = data.result; // ์„œ๋ฒ„์—์„œ ๋ฐ›์€ ๊ฒฐ๊ณผ๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œ
            first = data.result.toString(); // ๊ฒฐ๊ณผ๋ฅผ ์ฒซ ๋ฒˆ์งธ ์ˆซ์ž๋กœ ์ €์žฅํ•˜์—ฌ ์ด์–ด์„œ ๊ณ„์‚ฐ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
            second = '';       // ๋‘ ๋ฒˆ์งธ ์ˆซ์ž ์ดˆ๊ธฐํ™”
            operator = null;   // ์—ฐ์‚ฐ์ž ์ดˆ๊ธฐํ™”
            resultDisplayed = true;  // ๊ฒฐ๊ณผ ํ‘œ์‹œ ์™„๋ฃŒ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ
        })
        .catch(error => {
            console.error(error);  // ์˜ค๋ฅ˜ ์ฝ˜์†” ์ถœ๋ ฅ
            display.textContent = 'Error';  // ํ™”๋ฉด์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
        })
    }
);

โ–ถ๏ธ demo

profile
๐Ÿ—‚๏ธ hamstern

0๊ฐœ์˜ ๋Œ“๊ธ€