0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[シンプルハンズオン]AmazonLinux2023でiPhone電卓っぽいものを作ってみた

Posted at

AWSのAmazonLinux上にiPhoneの電卓のようなアプリを作ってみました。
(※内容はChatGPTに質問して返されたものです。)

全体構成

・OS:Amazon Linux 2023
・Webサーバ:Apache HTTP Server(mod_proxy_ajpでTomcatへ中継)
・アプリサーバ:Apache Tomcat 10
・Java:OpenJDK 17
・アプリ:電卓(HTML+Servlet)

※2025年11月現在の前提です。EC2の作成手順は割愛します。

1. 環境構築手順

(1) 依存パッケージとJavaの導入

sudo dnf update -y
sudo dnf install -y java-17-amazon-corretto-devel

(2) Apache HTTP Server のインストール

sudo dnf install -y httpd
sudo systemctl enable httpd
sudo systemctl start httpd

ブラウザで http://EC2のパブリックIP/ にアクセスし、「It works!」と出ればOK。

(3) Tomcat のインストール

cd /opt
sudo wget https://archive.apache.org/dist/tomcat/tomcat-10/v10.1.30/bin/apache-tomcat-10.1.30.tar.gz
sudo tar xzf apache-tomcat-10.1.30.tar.gz
sudo mv apache-tomcat-10.1.30 tomcat
sudo chown -R ec2-user:ec2-user /opt/tomcat

(4) Tomcat の起動

/opt/tomcat/bin/startup.sh

ブラウザで
http://EC2のパブリックIP:8080 にアクセスし、Tomcatのトップ画面が表示されればOK。

(5) Apache と Tomcat の連携(mod_proxy_ajp設定)

/etc/httpd/conf.d/tomcat_proxy.confを新規で作成して下記を記述。

<VirtualHost *:80>
    ServerName localhost
    ProxyRequests Off

    # AJP経由でTomcatへ転送
    ProxyPass / ajp://localhost:8009/
    ProxyPassReverse / ajp://localhost:8009/

    # ログ出力
    ErrorLog /var/log/httpd/tomcat_proxy_error.log
    CustomLog /var/log/httpd/tomcat_proxy_access.log combined
</VirtualHost>

AJPを使ってApache(httpd)とTomcatを連携する。
/opt/tomcat/conf/server.xmlを下記のように修正。
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector protocol="AJP/1.3"
           address="127.0.0.1"
           port="8009"
           redirectPort="8443"
           maxParameterCount="1000"
           secretRequired="false" />

<!-- Define an AJP 1.3 Connector on port 8009 --> の、1行下の
    <!--
と、           secretRequired="false" /> の、1行下の
    -->
は、削除してください。

Apache(httpd)サービスの再起動を実施。

sudo systemctl restart httpd

Tomcatサービスの再起動を実施。
/opt/tomcat/bin/shutdown.sh
/opt/tomcat/bin/startup.sh

→ これで http://EC2のパブリックIP/ にアクセスすると Apache経由でTomcatのアプリに到達します。


2. 電卓環境構築

(1)ディレクトリ構成を作る

Tomcat の標準アプリ配置先 /opt/tomcat/webapps/ にアプリフォルダを作ります。

cd /opt/tomcat/webapps/
mkdir -p calc/WEB-INF/classes

構成は下記のようになります。

/opt/tomcat/webapps/calc/
└ index.html(あとで作成)
└ WEB-INF/
   └ web.xml(あとで作成)
   └ classes/
       └ CalcServlet.java(あとで作成)

(2)画面の作成

/opt/tomcat/webapps/calc/index.htmlを新規作成して下記を記載します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
  <title>電卓</title>
  <style>
    :root { --bg:#111; --fg:#fff; --sub:#9aa0a6; --key:#2a2a2a; --op:#ff9500; --acc:#4a4a4a; }
    * { box-sizing:border-box; -webkit-tap-highlight-color: transparent; }
    body {
      margin:0; background:var(--bg); color:var(--fg);
      font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans JP", sans-serif;
      display:grid; place-items:center; height:100dvh;
    }
    .calc {
      width:min(420px, 100vw);
      padding:16px 16px 28px;
      display:grid; gap:12px;
    }
    .display {
      width:100%; min-height:96px; background:#000; border-radius:18px; padding:18px;
      display:flex; align-items:flex-end; justify-content:flex-end; overflow:hidden;
      box-shadow: inset 0 0 0 1px #222;
    }
    .expr { font-size:16px; color:var(--sub); margin-right:8px; user-select:none; }
    .value { font-size:42px; font-weight:600; word-break:break-all; }
    .keys {
      display:grid; gap:12px;
      grid-template-columns: repeat(4, 1fr);
    }
    button {
      height:64px; border:none; border-radius:18px; font-size:22px; font-weight:600;
      background:var(--key); color:var(--fg); cursor:pointer;
      box-shadow: 0 2px 0 #0007;
      transition: transform .03s ease-out, filter .1s;
    }
    button:active { transform: translateY(1px); filter:brightness(1.1); }
    .op { background:var(--op); color:#fff; }
    .acc { background:var(--acc); color:#fff; }
    .wide { grid-column: span 2; }
    .zero { grid-column: span 2; text-align:left; padding-left:22px; }
  </style>
</head>
<body>
  <div class="calc">
    <div class="display">
      <div class="expr" id="expr"></div>
      <div class="value" id="value">0</div>
    </div>
    <div class="keys">
      <button class="acc" data-act="clear">AC</button>
      <button class="acc" data-act="sign">±</button>
      <button class="acc" data-act="percent">%</button>
      <button class="op" data-op="÷">÷</button>

      <button data-num="7">7</button>
      <button data-num="8">8</button>
      <button data-num="9">9</button>
      <button class="op" data-op="×">×</button>

      <button data-num="4">4</button>
      <button data-num="5">5</button>
      <button data-num="6">6</button>
      <button class="op" data-op="-">-</button>

      <button data-num="1">1</button>
      <button data-num="2">2</button>
      <button data-num="3">3</button>
      <button class="op" data-op="+">+</button>

      <button class="zero" data-num="0">0</button>
      <button data-dot=".">.</button>
      <button class="op wide" data-act="equals">=</button>
    </div>
  </div>

  <script>
    const v = document.getElementById('value');
    const e = document.getElementById('expr');

    let num1 = null, op = null, typing = false;

    const mapToServer = (symbol) => ({'+':'+','-':'-','×':'*','÷':'/'}[symbol] || symbol);

    const setValue = (txt) => v.textContent = txt;
    const setExpr = (txt) => e.textContent = txt;

    const appendDigit = (d) => {
      if (!typing || v.textContent === '0') { setValue(d); typing = true; }
      else { setValue(v.textContent + d); }
    };

    const appendDot = () => {
      if (!typing) { setValue('0.'); typing = true; return; }
      if (!v.textContent.includes('.')) setValue(v.textContent + '.');
    };

    const chooseOp = (symbol) => {
      if (op && typing) {
        // 直前の計算を即時確定(連続演算対応)
        doEquals().then(() => {
          num1 = parseFloat(v.textContent);
          op = symbol;
          setExpr(num1 + ' ' + symbol);
          typing = false;
        });
        return;
      }
      num1 = parseFloat(v.textContent);
      op = symbol;
      setExpr(num1 + ' ' + symbol);
      typing = false;
    };

    const clearAll = () => {
      num1 = null; op = null; typing = false;
      setExpr(''); setValue('0');
    };

    const toggleSign = () => {
      if (v.textContent === '0') return;
      if (v.textContent.startsWith('-')) setValue(v.textContent.slice(1));
      else setValue('-' + v.textContent);
    };

    const percent = () => {
      const n = parseFloat(v.textContent || '0') / 100;
      setValue(String(n));
    };

    async function doEquals() {
      if (op == null || num1 == null) return;
      const num2 = parseFloat(v.textContent || '0');
      setExpr(num1 + ' ' + op + ' ' + num2 + ' =');

      // AJAXでサーブレットへ(JSONを優先して受け取る)
      const form = new URLSearchParams();
      form.set('num1', String(num1));
      form.set('num2', String(num2));
      form.set('op', mapToServer(op));

      try {
        const res = await fetch('calc', {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },
          body: form.toString()
        });

        let resultText;
        const ct = res.headers.get('content-type') || '';
        if (ct.includes('application/json')) {
          const json = await res.json();
          resultText = String(json.result);
        } else {
          // 旧HTML応答の場合は簡易パース(フォールバック)
          const html = await res.text();
          const m = html.match(/<b>(.*?)<\/b>/i);
          resultText = m ? m[1] : 'NaN';
        }
        setValue(resultText);
        num1 = null; op = null; typing = false;
      } catch (err) {
        setValue('Error');
        console.error(err);
      }
    }

    // ボタンイベント
    document.addEventListener('click', (ev) => {
      const b = ev.target.closest('button'); if (!b) return;
      if (b.dataset.num) return appendDigit(b.dataset.num);
      if (b.dataset.dot) return appendDot();
      if (b.dataset.op) return chooseOp(b.dataset.op);
      if (b.dataset.act === 'clear') return clearAll();
      if (b.dataset.act === 'sign') return toggleSign();
      if (b.dataset.act === 'percent') return percent();
      if (b.dataset.act === 'equals') return doEquals();
    });

    // キー入力(PC操作の補助)
    window.addEventListener('keydown', (ev) => {
      const k = ev.key;
      if (/\d/.test(k)) return appendDigit(k);
      if (k === '.') return appendDot();
      if (k === '+' || k === '-' || k === '*' || k === '/') {
        const inv = { '+':'+','-':'-','*':'×','/':'÷' }; return chooseOp(inv[k]);
      }
      if (k === 'Enter' || k === '=') return doEquals();
      if (k.toLowerCase() === 'c' || k === 'Escape') return clearAll();
    });
  </script>
</body>
</html>


(3)サーブレット (CalcServlet.java)の作成・コンパイル

/opt/tomcat/webapps/calc/WEB-INF/web.xmlを新規作成して下記を記載します。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">

  <servlet>
    <servlet-name>CalcServlet</servlet-name>
    <servlet-class>CalcServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>CalcServlet</servlet-name>
    <url-pattern>/calc</url-pattern>
  </servlet-mapping>

</web-app>

/opt/tomcat/webapps/calc/WEB-INF/classes/CalcServlet.javaを新規作成して下記を記載します。

import java.io.*;
import jakarta.servlet.*;
import jakarta.servlet.http.*;

public class CalcServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        double num1 = parse(request.getParameter("num1"));
        double num2 = parse(request.getParameter("num2"));
        String op = request.getParameter("op");
        double result = calc(num1, num2, op);

        String accept = request.getHeader("Accept");
        if (accept != null && accept.contains("application/json")) {
            response.setContentType("application/json; charset=UTF-8");
            try (PrintWriter out = response.getWriter()) {
                // 非常にシンプルなJSON生成(ライブラリなし)
                out.printf("{\"num1\":%s,\"num2\":%s,\"op\":\"%s\",\"result\":%s}",
                        stripNaN(num1), stripNaN(num2), escape(op), stripNaN(result));
            }
        } else {
            response.setContentType("text/html; charset=UTF-8");
            try (PrintWriter out = response.getWriter()) {
                out.println("<html><body>");
                out.println("<h2>計算結果</h2>");
                out.println("<p>" + num1 + " " + op + " " + num2 + " = <b>" + result + "</b></p>");
                out.println("<a href=\"index.html\">戻る</a>");
                out.println("</body></html>");
            }
        }
    }

    private static double parse(String s) {
        try { return Double.parseDouble(s); } catch (Exception e) { return Double.NaN; }
    }
    private static double calc(double a, double b, String op) {
        if (op == null) return Double.NaN;
        switch (op) {
            case "+": return a + b;
            case "-": return a - b;
            case "*": return a * b;
            case "/": return b != 0 ? a / b : Double.NaN;
            default:   return Double.NaN;
        }
    }
    private static String escape(String s) {
        if (s == null) return "";
        return s.replace("\\","\\\\").replace("\"","\\\"");
    }
    private static String stripNaN(double d) {
        if (Double.isNaN(d) || Double.isInfinite(d)) return "\"NaN\"";
        return Double.toString(d);
    }
}


下記コマンドでコンパイルを実施します。

cd /opt/tomcat/webapps/calc/WEB-INF/classes
javac -classpath /opt/tomcat/lib/servlet-api.jar CalcServlet.java

Tomcatを再起動します。
/opt/tomcat/bin/shutdown.sh
/opt/tomcat/bin/startup.sh


http://EC2のパブリックIP/calc/ を開くと画面が表示されます。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?