<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>怠惰ローカルブログ</title>
    <link>/</link>
    <description>Recent content on 怠惰ローカルブログ</description>
    <generator>Hugo -- 0.147.7</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 11 Apr 2026 00:00:00 +0900</lastBuildDate>
    <atom:link href="/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>WaylandでモニターがマイクとスピーカーとしてOSに認識される問題をWirePlumberで無効化する</title>
      <link>/posts/2026-04-11-disabled-microphone-and-speaker-in-wayland/</link>
      <pubDate>Sat, 11 Apr 2026 00:00:00 +0900</pubDate>
      <guid>/posts/2026-04-11-disabled-microphone-and-speaker-in-wayland/</guid>
      <description>&lt;h2 id=&#34;問題&#34;&gt;問題&lt;/h2&gt;
&lt;p&gt;ふとDesktopの画面を見るとモニターがマイクとスピーカーとしてOSに認識されていた。
誤って爆音で音が再生されるリスクが気になったため無効化することにした。
ついでにマイクも有効にする意味がない環境だったので止めた。&lt;/p&gt;
&lt;p&gt;純粋な開発PCで動画とかも見ないそこそこ特殊？な環境なので最悪読み込まないなら何でもいい状態。&lt;/p&gt;
&lt;h2 id=&#34;環境&#34;&gt;環境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OS: ArchLinux&lt;/li&gt;
&lt;li&gt;サウンドサーバー: PipeWire + WirePlumber 0.5.14&lt;/li&gt;
&lt;li&gt;GPU: AMD Ryzen（APU）&lt;/li&gt;
&lt;li&gt;問題のデバイス: &lt;code&gt;AMD/ATI Raven/Raven2/Fenghuang HDMI/DP Audio Controller&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;原因&#34;&gt;原因&lt;/h2&gt;
&lt;p&gt;HDMI/DisplayPortには映像だけでなく音声も伝送できる仕様（Audio over HDMI）がある。
LinuxはこれをALSAレベルで別サウンドカードとして認識するため、
PipeWireがそのまま拾ってオーディオデバイスとして公開してしまう。&lt;/p&gt;
&lt;h2 id=&#34;調査&#34;&gt;調査&lt;/h2&gt;
&lt;h3 id=&#34;認識されているカードを確認&#34;&gt;認識されているカードを確認&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pactl list cards short
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;49      alsa_card.pci-0000_04_00.1      alsa
50      alsa_card.pci-0000_04_00.6      alsa
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;2枚のサウンドカードが認識されている。詳細を確認する。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pactl list cards | grep -A &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;alsa_card.pci-0000_04_00&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;結果を整理すると：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;PCI アドレス&lt;/th&gt;
          &lt;th&gt;ベンダー&lt;/th&gt;
          &lt;th&gt;説明&lt;/th&gt;
          &lt;th&gt;用途&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;0000:04:00.1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;AMD/ATI&lt;/td&gt;
          &lt;td&gt;Raven HDMI/DP Audio Controller&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;モニター側（不要）&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;0000:04:00.6&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;AMD + Realtek ALC269VB&lt;/td&gt;
          &lt;td&gt;Ryzen HD Audio Controller&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;本物のオンボードサウンド&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;0000:04:00.1&lt;/code&gt; の &lt;code&gt;alsa_mixer_name&lt;/code&gt; が &lt;code&gt;ATI R6xx HDMI&lt;/code&gt; であることからも、
これがHDMI経由のオーディオデバイスだと確定できる。&lt;/p&gt;</description>
    </item>
    <item>
      <title>AdGuard HomeをProxmox LXCに立ててTailscale経由でDNSブロックする</title>
      <link>/posts/2026-04-06-tailscale-proxmox-adguard/</link>
      <pubDate>Mon, 06 Apr 2026 07:00:00 +0900</pubDate>
      <guid>/posts/2026-04-06-tailscale-proxmox-adguard/</guid>
      <description>&lt;h2 id=&#34;概要&#34;&gt;概要&lt;/h2&gt;
&lt;p&gt;はてな匿名ダイアリーとウーバーイーツをインフラレベルで封鎖したかった。
AdGuard HomeをProxmox LXCに立てて、Tailscale経由でDNSブロックする構成を作った。&lt;/p&gt;
&lt;h2 id=&#34;環境&#34;&gt;環境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Proxmox VE&lt;/li&gt;
&lt;li&gt;Tailscale導入済み&lt;/li&gt;
&lt;li&gt;AdGuard Home v0.108.0&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;手順&#34;&gt;手順&lt;/h2&gt;
&lt;h3 id=&#34;1-adguard-home-lxcをスクリプト一発で作成&#34;&gt;1. AdGuard Home LXCをスクリプト一発で作成&lt;/h3&gt;
&lt;p&gt;Proxmoxのノードシェルで以下を実行する。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bash -c &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/adguard.sh&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href=&#34;https://github.com/community-scripts/ProxmoxVE&#34;&gt;community-scripts/ProxmoxVE&lt;/a&gt;が提供するスクリプト。
LXCのコンテナ作成からAdGuard Homeのインストールまで全自動でやってくれる。&lt;/p&gt;
&lt;p&gt;デフォルト構成はDebian 13、CPU 1コア、RAM 512MB、HDD 2GB。DNS用途なら十分。&lt;/p&gt;
&lt;h3 id=&#34;2-lxcにtunデバイスを追加する&#34;&gt;2. LXCにTUNデバイスを追加する&lt;/h3&gt;
&lt;p&gt;TailscaleはWireGuardベースのVPNで、動作に&lt;code&gt;/dev/net/tun&lt;/code&gt;が必要。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/dev/net/tun&lt;/code&gt;はLinuxの仮想ネットワークデバイス（TUNデバイス）。通常のネットワークデバイス（eth0等）は物理NICに紐づいているが、TUNはソフトウェアで作った仮想NIC。&lt;/p&gt;
&lt;p&gt;Tailscaleは以下の流れで通信を処理する。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通信をTailscaleプロセスが横取り&lt;/li&gt;
&lt;li&gt;WireGuardで暗号化&lt;/li&gt;
&lt;li&gt;暗号化したパケットを相手に送る&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;この「横取り」の実装に&lt;code&gt;/dev/net/tun&lt;/code&gt;を使う。TUNデバイスを通してカーネルのネットワークスタックとTailscaleプロセスがやり取りする仕組みになっている。&lt;/p&gt;
&lt;p&gt;unprivileged LXCはセキュリティ上の理由でホストのデバイスに触れないようになっているため、明示的に&lt;code&gt;/dev/net/tun&lt;/code&gt;をコンテナに見せてあげる必要がある。&lt;/p&gt;
&lt;p&gt;Proxmoxのノードシェルで以下を実行してTUNを有効化する。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pct stop &lt;span style=&#34;color:#ae81ff&#34;&gt;106&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lxc.cgroup2.devices.allow: c 10:200 rwm&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/pve/lxc/106.conf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/pve/lxc/106.conf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pct start &lt;span style=&#34;color:#ae81ff&#34;&gt;106&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;106&lt;/code&gt;の部分は自分のLXCのIDに置き換える。IDは&lt;code&gt;pct list&lt;/code&gt;で確認できる。&lt;/p&gt;
&lt;h3 id=&#34;3-lxcにtailscaleを入れる&#34;&gt;3. LXCにTailscaleを入れる&lt;/h3&gt;
&lt;p&gt;LXCのシェルに入って以下を実行する。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;curl -fsSL https://tailscale.com/install.sh | sh
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tailscale up
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;認証URLが表示されるのでブラウザで開いてログインする。
認証後、TailscaleのMachines画面からAdGuard HomeのTailscale IPを確認しておく。&lt;/p&gt;</description>
    </item>
    <item>
      <title>ArchLinuxのThunarでWalkmanのFSを開く</title>
      <link>/posts/2026-04-04-linux-walkman-thunar/</link>
      <pubDate>Sat, 04 Apr 2026 17:00:00 +0900</pubDate>
      <guid>/posts/2026-04-04-linux-walkman-thunar/</guid>
      <description>&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;
&lt;p&gt;手持ちのWalkmanをLinux（Arch Linux）環境で活用したいと考えた。
単に音楽を聴くだけでなく、PCのファイルを転送したり、時にはPCの音を高音質で鳴らすオーディオインターフェースとして使いこなすのが目的だ。&lt;/p&gt;
&lt;p&gt;ドキュメントを読む限り、最近のデバイスはMTP（Media Transfer Protocol）に対応しており、Linuxでも標準的なツールで扱えるはずだ。&lt;/p&gt;
&lt;h3 id=&#34;環境&#34;&gt;環境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS&lt;/strong&gt;: Arch Linux&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File Manager&lt;/strong&gt;: Thunar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Device&lt;/strong&gt;: Walkman (MTP/USB DAC対応モデル)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt;: &lt;code&gt;usbutils&lt;/code&gt;, &lt;code&gt;gvfs-mtp&lt;/code&gt;, &lt;code&gt;libmtp&lt;/code&gt;, &lt;code&gt;jmtpfs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;thunarでwalkmanのfsが見えない&#34;&gt;ThunarでWalkmanのFSが見えない&lt;/h2&gt;
&lt;p&gt;WalkmanをUSBケーブルでPCに接続し、Thunarを開いたがサイドバーには何も表示されない。&lt;/p&gt;
&lt;p&gt;まず物理的な接続を確認しようと &lt;code&gt;lsusb&lt;/code&gt; を叩いたところ、コマンド自体が入っていなかった。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo pacman -S usbutils
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;改めて確認する。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ lsusb
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Bus &lt;span style=&#34;color:#ae81ff&#34;&gt;001&lt;/span&gt; Device 008: ID 054c:0c2f Sony Corp. Walkman
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;デバイス自体はUSBレベルでは認識されている。&lt;code&gt;fdisk -l&lt;/code&gt; にブロックデバイスとして出てこないのはMTPなので当然だ。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;原因mtp用ライブラリが未インストール&#34;&gt;原因：MTP用ライブラリが未インストール&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;gvfs-mtp&lt;/code&gt; と &lt;code&gt;libmtp&lt;/code&gt; が入っていないのが原因だった。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo pacman -S gvfs-mtp libmtp
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Thunarを再起動して反映&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;thunar -q
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;これでThunarのサイドバーにWalkmanが表示され、GUIでファイルをコピーできるようになった。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;補足usb-dacモードとは&#34;&gt;補足：USB DACモードとは&lt;/h2&gt;
&lt;p&gt;調査中に「DACモードでなければ動かないのか？」と気になって調べたのでここにまとめておく。&lt;/p&gt;
&lt;p&gt;USB DACモードとは、デバイスを「ストレージ」としてではなく、**「USBオーディオデバイス」**としてPCに認識させるモードだ。ファイル転送には使えない。&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;モード&lt;/th&gt;
          &lt;th&gt;PCからの見え方&lt;/th&gt;
          &lt;th&gt;用途&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;MTP / MSC&lt;/td&gt;
          &lt;td&gt;ストレージ&lt;/td&gt;
          &lt;td&gt;ファイル転送&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;USB DAC&lt;/td&gt;
          &lt;td&gt;オーディオデバイス&lt;/td&gt;
          &lt;td&gt;PC音声出力&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Walkman側の設定でどちらのモードになっているかは確認しておく必要がある。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Canvas 2D API でピクセルアートタイルを「手書き」する実装テクニック</title>
      <link>/posts/2026-03-28-canvas-pixel-tile/</link>
      <pubDate>Tue, 31 Mar 2026 09:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-canvas-pixel-tile/</guid>
      <description>&lt;h1 id=&#34;canvas-2d-api-でピクセルアートタイルを手書きする実装テクニック&#34;&gt;Canvas 2D API でピクセルアートタイルを「手書き」する実装テクニック&lt;/h1&gt;
&lt;h2 id=&#34;1-概要&#34;&gt;1. 概要&lt;/h2&gt;
&lt;p&gt;Web ゲーム開発、特にローグライクやタクティカル RPG を開発する際、避けて通れないのが「マップの描画」だ。通常、これらは「タイルセット」と呼ばれる画像ファイルを読み込んで描画するが、開発の初期段階や、あえて外部アセットに頼りたくない場合、Canvas 2D API を使ってプログラムで直接タイルを描画する「手書き（プログラマティック描画）」の手法が非常に強力な武器になる。&lt;/p&gt;
&lt;p&gt;本記事では、HTML5 Canvas の &lt;code&gt;fillRect&lt;/code&gt; や &lt;code&gt;beginPath&lt;/code&gt; などの基本命令のみを使い、擬似的なピクセルアート風のタイルを描画するテクニックを解説する。画像を用意する手間を省きつつ、動的に色や形状を変更できる柔軟な描画システムを構築しよう。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-タイル描画の基本構造&#34;&gt;2. タイル描画の基本構造&lt;/h2&gt;
&lt;p&gt;まずは、どのようなタイルでも共通して利用できる描画の入り口を作る。ピクセル座標ではなく、タイル座標（x, y）とタイルサイズ（tileSize）を受け取る設計にすることで、グリッドベースのシステムと統合しやすくなる。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * タイルを描画するメイン関数
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * @param {CanvasRenderingContext2D} ctx - Canvasコンテキスト
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * @param {string} tileType - タイルの種類 (&amp;#39;wall&amp;#39;, &amp;#39;floor&amp;#39;, &amp;#39;grass&amp;#39;, etc.)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * @param {number} x - タイルのX座標（グリッド単位）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * @param {number} y - タイルのY座標（グリッド単位）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * @param {number} tileSize - 1タイルのピクセルサイズ
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * @param {string} fieldType - フィールドの種類 (&amp;#39;meadow&amp;#39;, &amp;#39;forest&amp;#39;, &amp;#39;mountain&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;drawTile&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileType&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fieldType&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;meadow&amp;#39;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;px&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;py&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;translate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;px&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;py&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;switch&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;tileType&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;floor&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;drawFloor&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fieldType&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;wall&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;drawWall&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fieldType&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;object_grass&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;drawFloor&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fieldType&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;drawGrass&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;object_tree&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;drawFloor&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fieldType&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;drawTree&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;stairs_down&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;drawFloor&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fieldType&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;drawStairs&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;default&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fillStyle&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;#333&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fillRect&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tileSize&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;restore&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;この設計のポイントは、&lt;code&gt;ctx.translate&lt;/code&gt; を使ってタイルの左上を原点 (0, 0) に固定することだ。これにより、各タイルの描画ロジック内で座標計算を簡略化できる。&lt;/p&gt;</description>
    </item>
    <item>
      <title>TypeScript Union 型でスキルのレンジ種別とダメージ計算を型安全に実装する</title>
      <link>/posts/2026-03-28-typescript-union-skill-design/</link>
      <pubDate>Sat, 28 Mar 2026 17:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-typescript-union-skill-design/</guid>
      <description>&lt;h1 id=&#34;typescript-union-型でスキルのレンジ種別とダメージ計算を型安全に実装する&#34;&gt;TypeScript Union 型でスキルのレンジ種別とダメージ計算を型安全に実装する&lt;/h1&gt;
&lt;p&gt;ゲーム開発、特に RPG やタクティカルゲームにおいて、スキルの「射程（レンジ）」や「効果範囲（AOE: Area of Effect）」の実装は非常に複雑になりがちです。単体攻撃、直線、周囲、円形、さらには自分自身を対象とするものまで、そのバリエーションは多岐にわたります。&lt;/p&gt;
&lt;p&gt;これらを &lt;code&gt;string&lt;/code&gt; 型の ID や、大量の &lt;code&gt;if&lt;/code&gt; 文、あるいは複雑なクラス継承で管理しようとすると、いつか必ず「新しいレンジを追加したのに、判定処理を書き忘れた」あるいは「このスキルタイプには不要なはずのパラメータが混入している」といったバグに直面します。&lt;/p&gt;
&lt;p&gt;TypeScript の &lt;strong&gt;Union 型（特に Discriminated Union / 判別可能な共用体）&lt;/strong&gt; を活用すれば、こうしたロジックをコンパイルレベルで安全に保護し、設計意図をコードに焼き付けることができます。本記事では、架空のゲームを題材に、&lt;code&gt;any&lt;/code&gt; や &lt;code&gt;as&lt;/code&gt; を一切使わずに、メンテナンス性の高いスキルシステムを構築する手法を徹底解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-union-型でスキル種別を表現する&#34;&gt;1. Union 型でスキル種別を表現する&lt;/h2&gt;
&lt;p&gt;まず、スキルの「レンジ（範囲）」を型として定義します。ここでのポイントは、単に名前を列挙するのではなく、&lt;strong&gt;「そのレンジを計算するために最低限必要なデータ」をセットにする&lt;/strong&gt;ことです。&lt;/p&gt;
&lt;h3 id=&#34;なぜこの設計にするのかデータ構造の純粋性&#34;&gt;なぜこの設計にするのか（データ構造の純粋性）&lt;/h3&gt;
&lt;p&gt;オブジェクト指向的な発想では、&lt;code&gt;BaseRange&lt;/code&gt; クラスを継承して &lt;code&gt;AreaRange&lt;/code&gt; クラスを作る、といった方法が取られがちです。しかし、ゲームデータ（特に JSON などでシリアライズされるデータ）を扱う場合、純粋なオブジェクト（POJO）として扱える方がシリアリアライズの相性が良く、ロジックを分離（疎結合）しやすくなります。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 1. 各レンジの個別定義
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MeleeRange&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;melee&amp;#39;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}; &lt;span style=&#34;color:#75715e&#34;&gt;// 隣接マス（1マス）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RangedRange&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ranged&amp;#39;&lt;/span&gt;; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;distance&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}; &lt;span style=&#34;color:#75715e&#34;&gt;// 遠距離（単体指定）。最大射程が必要。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;LineRange&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;line&amp;#39;&lt;/span&gt;; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;width?&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;; &lt;span style=&#34;color:#75715e&#34;&gt;// オプションで太さを持たせることも可能
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;}; &lt;span style=&#34;color:#75715e&#34;&gt;// 直線。貫通距離が必要。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AreaRange&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;area&amp;#39;&lt;/span&gt;; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;radius&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;excludeCenter?&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;boolean&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}; &lt;span style=&#34;color:#75715e&#34;&gt;// 円形または四角形範囲。半径が必要。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SurroundRange&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;surround&amp;#39;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}; &lt;span style=&#34;color:#75715e&#34;&gt;// 自分の周囲8マス。パラメータ不要。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SelfRange&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;self&amp;#39;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}; &lt;span style=&#34;color:#75715e&#34;&gt;// 自分自身。パラメータ不要。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 2. これらを統合した Union 型（Discriminated Union）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SkillRange&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MeleeRange&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RangedRange&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;LineRange&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AreaRange&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SurroundRange&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SelfRange&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 3. スキルの全体定義
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Skill&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;description&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;range&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;SkillRange&lt;/span&gt;; &lt;span style=&#34;color:#75715e&#34;&gt;// 型安全なレンジ定義
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;baseDamage&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;scalingStat&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;STR&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;INT&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;DEX&amp;#39;&lt;/span&gt;; &lt;span style=&#34;color:#75715e&#34;&gt;// どのステータスに依存するか
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;manaCost&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;このように &lt;code&gt;type&lt;/code&gt; という「タグ（判別子）」を持たせることで、TypeScript は「&lt;code&gt;type&lt;/code&gt; が &lt;code&gt;&#39;area&#39;&lt;/code&gt; なら &lt;code&gt;radius&lt;/code&gt; プロパティが確実に存在する」と認識します。逆に、&lt;code&gt;&#39;melee&#39;&lt;/code&gt; の時には &lt;code&gt;radius&lt;/code&gt; にアクセスしようとするとコンパイルエラーになります。これにより、不必要なデータへの依存を完全に排除できます。&lt;/p&gt;</description>
    </item>
    <item>
      <title>ターン制戦闘をフィールドに統合する vs 専用画面に分ける：設計比較と判断基準</title>
      <link>/posts/2026-03-28-turn-based-combat-patterns/</link>
      <pubDate>Sat, 28 Mar 2026 16:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-turn-based-combat-patterns/</guid>
      <description>&lt;h1 id=&#34;ターン制戦闘をフィールドに統合する-vs-専用画面に分ける設計比較と判断基準&#34;&gt;ターン制戦闘をフィールドに統合する vs 専用画面に分ける：設計比較と判断基準&lt;/h1&gt;
&lt;p&gt;ターン制RPGを開発する際、エンジニアが最初に直面する大きな設計判断の一つが「戦闘をどこで行うか」です。具体的には、不思議のダンジョンのように**フィールド上でそのまま戦う（フィールド統合型）&lt;strong&gt;のか、ドラゴンクエストのように&lt;/strong&gt;専用の戦闘画面に遷移する（専用画面型）**のか、という選択です。&lt;/p&gt;
&lt;p&gt;この選択は単なるビジュアルの違いに留まらず、状態管理（State Management）、当たり判定、AIの実装、そしてスケーラビリティに決定的な影響を与えます。本稿では、TypeScriptを用いた架空のゲームの実装例を交えながら、両アプローチの設計思想と判断基準を深く掘り下げます。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-2つのアプローチの比較&#34;&gt;1. 2つのアプローチの比較&lt;/h2&gt;
&lt;p&gt;まずは、それぞれの特性をトレードオフの観点から整理します。&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;項目&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;フィールド統合型 (Seamless)&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;専用画面型 (Isolated)&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;UXの印象&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;テンポが良い、空間の繋がりを感じる&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;演出が豪華、戦略に集中できる&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;状態管理&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;非常に複雑（フィールド＋戦闘の混合）&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;比較的単純（画面ごとにStateを全入れ替え）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;位置情報の意味&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;極めて重要（射程、視線、逃走経路）&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;抽象的（前衛・後衛、ターゲット選択）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;AIの実装コスト&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;高い（地形考慮、パス検索が必要）&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;低い（コマンド選択アルゴリズムに集中）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;拡張性&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;難しい（新しいギミックが戦闘に影響する）&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;容易（戦闘専用の特殊ルールを作りやすい）&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-フィールド統合型の実装空間と時間の同期&#34;&gt;2. フィールド統合型の実装：空間と時間の同期&lt;/h2&gt;
&lt;p&gt;フィールド統合型（ローグライク方式）では、「移動」と「攻撃」が同じタイムライン上で扱われます。&lt;/p&gt;
&lt;h3 id=&#34;なぜその設計にするか&#34;&gt;なぜその設計にするか&lt;/h3&gt;
&lt;p&gt;プレイヤーが「一歩動く」ことと「剣を振る」ことが同等のコスト（1ターン）を持つため、戦略が&lt;strong&gt;空間的&lt;/strong&gt;になります。壁を背にする、通路に誘い込むといった地形利用が自然にゲームプレイに組み込まれるのが最大のメリットです。&lt;/p&gt;
&lt;h3 id=&#34;アクション設計の例&#34;&gt;アクション設計の例&lt;/h3&gt;
&lt;p&gt;TypeScriptでのアクション定義は以下のようになります。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;FieldAction&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; { &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;FIELD_PLAYER_MOVE&amp;#39;&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;direction&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Vector2&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; { &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;FIELD_PLAYER_ATTACK&amp;#39;&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;targetId&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; { &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;FIELD_MONSTER_TURN_START&amp;#39;&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; { &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;FIELD_DAMAGE_ENTITY&amp;#39;&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;entityId&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;amount&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; { &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;FIELD_KILL_MONSTER&amp;#39;&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;monsterId&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;FieldState&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;player&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Player&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;monsters&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Record&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;string&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;Monster&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;tiles&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;TileMap&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;turnOwner&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;PLAYER&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;MONSTER&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;animations&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;AnimationQueue&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;実装のポイント&#34;&gt;実装のポイント&lt;/h3&gt;
&lt;p&gt;この形式では、&lt;code&gt;Reducer&lt;/code&gt; が非常に巨大になりがちです。なぜなら、「移動した結果、トラップを踏み、そのダメージでHPが0になり、死亡処理が走る」という一連の連鎖（Side Effects）を、同一のグリッド座標系で計算しなければならないからです。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fieldReducer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;FieldState&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;action&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;FieldAction&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;FieldState&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;switch&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;action&lt;/span&gt;.&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;FIELD_PLAYER_ATTACK&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;monster&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;monsters&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;action&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;targetId&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;monster&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#75715e&#34;&gt;// 距離計算が必須
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dist&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;calculateDistance&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;player&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;pos&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;monster&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;pos&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;dist&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;player&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;range&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ...&lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 戦闘結果を直接フィールドの状態に反映
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;monsters&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          ...&lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;monsters&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          [&lt;span style=&#34;color:#a6e22e&#34;&gt;action&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;targetId&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { ...&lt;span style=&#34;color:#a6e22e&#34;&gt;monster&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;hp&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;monster.hp&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;player&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;atk&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;animations&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [...&lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;animations&lt;/span&gt;, { &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;SLASH&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;pos&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;monster.pos&lt;/span&gt; }]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;3-専用画面型の実装コンテキストの分離&#34;&gt;3. 専用画面型の実装：コンテキストの分離&lt;/h2&gt;
&lt;p&gt;専用画面型（エンカウント方式）では、戦闘が開始された瞬間にフィールドのコンテキストがシリアライズされ、独立した「戦闘エンジン」に制御が移ります。&lt;/p&gt;</description>
    </item>
    <item>
      <title>React で作る堅牢なゲームセーブシステム：localStorage と Reducer を疎結合に保つ設計</title>
      <link>/posts/2026-03-28-save-load-system-reducer/</link>
      <pubDate>Sat, 28 Mar 2026 15:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-save-load-system-reducer/</guid>
      <description>&lt;h1 id=&#34;react-で作る堅牢なゲームセーブシステムlocalstorage-と-reducer-を疎結合に保つ設計&#34;&gt;React で作る堅牢なゲームセーブシステム：localStorage と Reducer を疎結合に保つ設計&lt;/h1&gt;
&lt;p&gt;ゲーム開発において、プレイヤーの進行状況を保存する「セーブ / ロード」は最も重要な機能の一つです。Web ブラウザで動作する React アプリケーションの場合、最も手軽な保存先は &lt;code&gt;localStorage&lt;/code&gt; です。&lt;/p&gt;
&lt;p&gt;しかし、単純に &lt;code&gt;localStorage.setItem&lt;/code&gt; をコードのあちこちに散りばめてしまうと、テスタビリティが低下し、データ構造の変更（スキーマ変更）に弱いシステムになってしまいます。&lt;/p&gt;
&lt;p&gt;本記事では、架空の RPG 『React Odyssey』を例に、&lt;code&gt;SavePort&lt;/code&gt; インターフェースと &lt;code&gt;Reducer&lt;/code&gt; の初期化関数を組み合わせた、クリーンで堅牢なセーブ / ロードの実装方法を解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-概要なぜ直接-localstorageを避けるのか&#34;&gt;1. 概要：なぜ「直接 localStorage」を避けるのか&lt;/h2&gt;
&lt;p&gt;React の &lt;code&gt;useReducer&lt;/code&gt; を使ったゲーム開発では、ゲームの状態（State）は一つの大きなオブジェクトとして管理されます。これを &lt;code&gt;JSON.stringify&lt;/code&gt; して &lt;code&gt;localStorage&lt;/code&gt; に保存するのは簡単です。&lt;/p&gt;
&lt;p&gt;しかし、以下の理由から、ロジックの中に直接 &lt;code&gt;localStorage&lt;/code&gt; を書くべきではありません。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;副作用の分離&lt;/strong&gt;: Reducer は純粋関数であるべきです。セーブ処理（副作用）を Reducer の中に入れることはできません。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;環境非依存&lt;/strong&gt;: 将来的に保存先を &lt;code&gt;IndexedDB&lt;/code&gt; やクラウド（Firebase 等）に変更したくなったとき、コードを大幅に書き換える必要が出てきます。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;テストのしやすさ&lt;/strong&gt;: &lt;code&gt;localStorage&lt;/code&gt; が存在しない Node.js 環境（Vitest 等）でロジックのテストを行う際、モック化が容易である必要があります。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;これらを解決するために、&lt;strong&gt;Dependency Inversion Principle（依存性逆転の原則）&lt;/strong&gt; に基づいた設計を採用します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-saveport-設計抽象化の定義&#34;&gt;2. SavePort 設計：抽象化の定義&lt;/h2&gt;
&lt;p&gt;まずは、ゲームエンジン側が必要とする「セーブ機能」のインターフェースを定義します。これを &lt;code&gt;SavePort&lt;/code&gt; と呼びます。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// domain/save/save-port.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/** 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * 保存されるデータの構造（スキーマ）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * ゲームの現在の状態に加え、メタ情報を付与する
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SaveData&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;version&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;;        &lt;span style=&#34;color:#75715e&#34;&gt;// セーブデータのバージョン
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;timestamp&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;      &lt;span style=&#34;color:#75715e&#34;&gt;// 保存日時
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;GameState&lt;/span&gt;;       &lt;span style=&#34;color:#75715e&#34;&gt;// 実際のゲーム状態
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * セーブ/ロードに関する抽象インターフェース
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SavePort&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** データを保存する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;SaveData&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;void&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** データを読み込む。存在しない場合は null を返す */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;load&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;SaveData&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;null&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** セーブデータが存在するか確認する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;exists&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;boolean&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** セーブデータを削除する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;clear&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;void&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;なぜインターフェースにするのか？&lt;/strong&gt;
ゲームのメインロジック（UseCase）は、この &lt;code&gt;SavePort&lt;/code&gt; を通じて読み書きを行います。この時点では「どうやって保存するか」は知らなくてよく、「保存できる」という事実だけに依存させます。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Reducer パターンだけでゲーム状態を管理する：副作用ゼロのアーキテクチャ</title>
      <link>/posts/2026-03-28-reducer-pure-state/</link>
      <pubDate>Sat, 28 Mar 2026 14:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-reducer-pure-state/</guid>
      <description>&lt;h1 id=&#34;reducer-パターンだけでゲーム状態を管理する副作用ゼロのアーキテクチャ&#34;&gt;Reducer パターンだけでゲーム状態を管理する：副作用ゼロのアーキテクチャ&lt;/h1&gt;
&lt;h2 id=&#34;概要&#34;&gt;概要&lt;/h2&gt;
&lt;p&gt;現代のフロントエンド開発において、Redux や React の &lt;code&gt;useReducer&lt;/code&gt; を通じて「Reducer パターン」は広く浸透しました。しかし、このパターンを本格的なゲーム開発、特に複雑なロジックが絡み合う RPG やシミュレーションゲームに適用しようとすると、多くの開発者が「副作用」の壁にぶつかります。&lt;/p&gt;
&lt;p&gt;「ダメージ計算に乱数を使いたい」「ゲーム内の経過時間を管理したい」「マスターデータを参照したい」……。&lt;/p&gt;
&lt;p&gt;これらの要素を素直に実装すると、Reducer の外側にある状態や関数に依存してしまい、純粋関数としての美しさとテストのしやすさが失われてしまいます。&lt;/p&gt;
&lt;p&gt;本記事では、すべての副作用を引数として注入し、&lt;strong&gt;&lt;code&gt;gameReducer(state, action, masters, random)&lt;/code&gt;&lt;/strong&gt; という純粋関数のみでゲームの全ロジックを完結させる「副作用ゼロ」のアーキテクチャについて解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;課題なぜゲーム開発で-reducer-は敬遠されるのか&#34;&gt;課題：なぜゲーム開発で Reducer は敬遠されるのか&lt;/h2&gt;
&lt;p&gt;一般的な Web アプリケーションの Reducer は単純です。「ボタンを押したらフラグを反転させる」「入力された文字列を状態に保存する」といった操作がメインだからです。&lt;/p&gt;
&lt;p&gt;しかし、ゲームでは以下のような「非決定的な要素」や「巨大な静的データ」が頻繁に登場します。&lt;/p&gt;
&lt;h3 id=&#34;1-乱数-randomness-の扱い&#34;&gt;1. 乱数 (Randomness) の扱い&lt;/h3&gt;
&lt;p&gt;クリティカルヒットの判定、モンスターのドロップアイテム、マップの自動生成など、ゲームは乱数の塊です。Reducer の中で &lt;code&gt;Math.random()&lt;/code&gt; を呼んだ瞬間、その関数は「同じ入力に対して同じ出力を返す」という純粋性を失います。&lt;/p&gt;
&lt;h3 id=&#34;2-静的データ-master-data-の参照&#34;&gt;2. 静的データ (Master Data) の参照&lt;/h3&gt;
&lt;p&gt;モンスターのステータス表、アイテムの定義、スキル効果など、ゲームには膨大な「変わらないデータ」があります。これを Reducer の外にあるグローバル変数から参照すると、テスト時にそのグローバル変数の状態も気にしなければならなくなります。&lt;/p&gt;
&lt;h3 id=&#34;3-時刻-time-と経過の管理&#34;&gt;3. 時刻 (Time) と経過の管理&lt;/h3&gt;
&lt;p&gt;「3時間経過したらスタミナが回復する」「夜になるとモンスターが強くなる」といった時間依存の処理です。&lt;code&gt;new Date()&lt;/code&gt; を Reducer で使うのは、乱数と同様に純粋性を破壊します。&lt;/p&gt;
&lt;h3 id=&#34;4-非同期処理-asyncfetch&#34;&gt;4. 非同期処理 (Async/Fetch)&lt;/h3&gt;
&lt;p&gt;サーバーから最新のイベント情報を取得したり、セーブデータをロードしたりする処理です。Reducer は同期的に実行される必要があるため、非同期処理をそのまま中に書くことはできません。&lt;/p&gt;
&lt;h3 id=&#34;oopオブジェクト指向との比較&#34;&gt;OOP（オブジェクト指向）との比較&lt;/h3&gt;
&lt;p&gt;クラスベースの OOP では、&lt;code&gt;player.attack(monster)&lt;/code&gt; のようにメソッドを呼び出します。これは直感的ですが、内部で &lt;code&gt;this.hp -= damage&lt;/code&gt; のように状態を直接書き換えます（ミュータブル）。
小規模なら良いですが、プロジェクトが大きくなり「攻撃時にスキルが発動し、その効果で回復し、さらにログに記録し……」と連鎖が始まると、どこで何が起きたかを追跡するのが不可能になります。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;設計副作用を引数に封じ込める&#34;&gt;設計：副作用を「引数」に封じ込める&lt;/h2&gt;
&lt;p&gt;副作用を排除するための解決策はシンプルです。&lt;strong&gt;「必要なものはすべて外から渡す」&lt;/strong&gt;、つまり依存性の注入 (DI) です。&lt;/p&gt;</description>
    </item>
    <item>
      <title>React &#43; Canvas 2D API で作るターン制ローグライク：論理と描画を切り離す設計</title>
      <link>/posts/2026-03-28-react-canvas-roguelike/</link>
      <pubDate>Sat, 28 Mar 2026 13:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-react-canvas-roguelike/</guid>
      <description>&lt;h1 id=&#34;react--canvas-2d-api-で作るターン制ローグライク論理と描画を切り離す設計&#34;&gt;React + Canvas 2D API で作るターン制ローグライク：論理と描画を切り離す設計&lt;/h1&gt;
&lt;p&gt;React でゲームを作る際、多くの開発者が最初に直面するのが「DOM で描画するか、Canvas で描画するか」という選択です。特に数千枚のタイルや多数のユニットが登場するローグライクゲームでは、DOM 要素の管理はすぐにパフォーマンスの限界に達します。&lt;/p&gt;
&lt;p&gt;本稿では、React の強力な状態管理（&lt;code&gt;useReducer&lt;/code&gt;）と、Canvas 2D API の命令的な描画を組み合わせ、滑らかなアニメーションを実現しつつ堅牢なゲームロジックを維持する設計手法について解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-概要なぜ-react-と-canvas-を組み合わせるのか&#34;&gt;1. 概要：なぜ React と Canvas を組み合わせるのか&lt;/h2&gt;
&lt;p&gt;React は「宣言的」な UI 構築に長けていますが、毎秒 60 回の頻度で数千の DOM 要素を更新するような動的な描画には向いていません。一方で、Canvas は「命令的」であり、ピクセル単位での高速な描画が可能ですが、状態と描画の同期を自分で行う必要があります。&lt;/p&gt;
&lt;p&gt;この二つの「いいとこ取り」をするのが、&lt;strong&gt;「ロジックは React（useReducer）で、描画は Canvas で」&lt;/strong&gt; という役割分担です。&lt;/p&gt;
&lt;h3 id=&#34;本記事で構築するアーキテクチャ&#34;&gt;本記事で構築するアーキテクチャ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Core Logic (&lt;code&gt;useReducer&lt;/code&gt;)&lt;/strong&gt;: ゲームの「真実の状態（State of Truth）」を管理。ターン単位で離散的に変化する座標などを扱う。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Render Loop (&lt;code&gt;requestAnimationFrame&lt;/code&gt;)&lt;/strong&gt;: Canvas 上で毎フレーム実行される描画処理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interpolation (補完)&lt;/strong&gt;: 離散的な論理座標を、滑らかな描画座標へと変換するローカル状態管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-設計判断論理座標と描画座標の分離&#34;&gt;2. 設計判断：論理座標と描画座標の分離&lt;/h2&gt;
&lt;p&gt;ローグライクゲームは基本的に「ターン制」です。プレイヤーが右に移動したとき、内部データ（Core State）では &lt;code&gt;x: 10&lt;/code&gt; から &lt;code&gt;x: 11&lt;/code&gt; へと一瞬で書き換わります。しかし、これをそのまま描画すると、キャラクターがワープしたように見えてしまいます。&lt;/p&gt;
&lt;p&gt;滑らかな移動（アニメーション）を実現するためには、以下の二種類の状態を明確に分ける必要があります。&lt;/p&gt;
&lt;h3 id=&#34;なぜ分離が必要か&#34;&gt;なぜ分離が必要か&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;種類&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;管理場所&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;特徴&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;役割&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;論理座標 (Logical Position)&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;useReducer&lt;/code&gt; (Global)&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;整数（タイル単位）。&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;当たり判定、AI、クエスト進行など。&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;描画座標 (Visual Position)&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Canvas 内の Actor クラス (Local)&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;小数点を含むピクセル単位。&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;滑らかな移動、揺れ、エフェクト。&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;判断理由：&lt;/strong&gt;
Core State にアニメーションの「途中経過（x: 10.2 など）」を持たせてしまうと、ゲームロジックが描画の都合に汚染されます。例えば、「まだ移動アニメーション中だから攻撃はできない」といった判定をロジック層で書く必要が出てき、コードが複雑化します。ロジック層は常に「今は（論理的に）どこにいるか」だけを知っていれば良いのです。&lt;/p&gt;</description>
    </item>
    <item>
      <title>乱数を interface で抽象化してゲームロジックをテスタブルにする</title>
      <link>/posts/2026-03-28-random-interface-testability/</link>
      <pubDate>Sat, 28 Mar 2026 12:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-random-interface-testability/</guid>
      <description>&lt;h1 id=&#34;乱数を-interface-で抽象化してゲームロジックをテスタブルにする&#34;&gt;乱数を interface で抽象化してゲームロジックをテスタブルにする&lt;/h1&gt;
&lt;h2 id=&#34;概要&#34;&gt;概要&lt;/h2&gt;
&lt;p&gt;ゲーム開発において、「運」の要素は面白さを生む不可欠なスパイスです。クリティカルヒット、レアアイテムのドロップ、ダンジョンの自動生成など、多くの場面で乱数が使われます。&lt;/p&gt;
&lt;p&gt;しかし、プログラミングの文脈において、乱数は「不確実性」そのものであり、ユニットテストの天敵です。本記事では、TypeScript を用いて乱数をインターフェースで抽象化し、テスト時に決定論的な（結果が予測可能な）挙動をさせる設計パターンについて解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;課題mathrandom-がもたらすテスト不能という病&#34;&gt;課題：&lt;code&gt;Math.random()&lt;/code&gt; がもたらす「テスト不能」という病&lt;/h2&gt;
&lt;p&gt;もっとも素浦に実装すると、ゲームロジックの中で直接 &lt;code&gt;Math.random()&lt;/code&gt; を呼び出すことになります。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 直接 Math.random() を使う例
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;CombatService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;calculateDamage&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;baseDamage&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;critRate&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 運が悪ければテストが落ちる
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;random&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;critRate&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;baseDamage&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;baseDamage&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;このコードをテストしようとすると、以下の問題に直面します。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;非決定的 (Non-deterministic) なテスト&lt;/strong&gt;: 同じ入力に対して、実行するたびに結果が変わる可能性があります。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;境界値のテストが困難&lt;/strong&gt;: 「クリティカル率 5% のとき、0.049 を引いたらクリティカル、0.051 を引いたら通常攻撃」という境界値の検証が運任せになります。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;モックの乱立&lt;/strong&gt;: &lt;code&gt;vi.spyOn(Math, &#39;random&#39;)&lt;/code&gt; などでグローバルなオブジェクトを書き換える手法もありますが、並列テストで干渉したり、クリーンアップを忘れると他のテストに影響を与えたりと、脆いテストになりがちです。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;設計port--adapter-パターンによる抽象化&#34;&gt;設計：Port / Adapter パターンによる抽象化&lt;/h2&gt;
&lt;p&gt;この問題を解決するために、&lt;strong&gt;Dependency Inversion Principle (依存性逆転の原則)&lt;/strong&gt; を適用します。&lt;/p&gt;
&lt;p&gt;ロジックが「具体的な乱数生成器」に依存するのではなく、抽象的な「乱数提供インターフェース」に依存するように設計を変更します。&lt;/p&gt;
&lt;h3 id=&#34;1-port-interface-の定義&#34;&gt;1. Port (Interface) の定義&lt;/h3&gt;
&lt;p&gt;ロジックが必要とする「乱数を得るための窓口」を定義します。&lt;/p&gt;
&lt;h3 id=&#34;2-adapter-implementation-の実装&#34;&gt;2. Adapter (Implementation) の実装&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Production Adapter&lt;/strong&gt;: 本番環境では &lt;code&gt;Math.random()&lt;/code&gt; を使う。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test Adapter&lt;/strong&gt;: テスト環境では、事前に定義した値を返したり、シード値に基づいた再現性のある乱数を返す。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;この設計にすることで、ロジック側は「誰がどうやって乱数を作っているか」を気にせず、「乱数がもらえること」だけを約束された状態になります。&lt;/p&gt;</description>
    </item>
    <item>
      <title>TypeScript npm workspaces でゲームロジックを UI から完全分離する</title>
      <link>/posts/2026-03-28-monorepo-logic-separation/</link>
      <pubDate>Sat, 28 Mar 2026 11:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-monorepo-logic-separation/</guid>
      <description>&lt;h1 id=&#34;typescript-npm-workspaces-でゲームロジックを-ui-から完全分離する&#34;&gt;TypeScript npm workspaces でゲームロジックを UI から完全分離する&lt;/h1&gt;
&lt;h2 id=&#34;概要&#34;&gt;概要&lt;/h2&gt;
&lt;p&gt;モダンなフロントエンド開発、特にゲーム開発において、「ロジック」と「表示（UI）」の分離は永遠の課題です。React や Vue などのフレームワークにロジックが密結合してしまうと、テストが困難になり、将来的に別のプラットフォーム（例えば Web から React Native や CLI ツールへ）に展開する際の大きな障害となります。&lt;/p&gt;
&lt;p&gt;本記事では、&lt;strong&gt;TypeScript npm workspaces&lt;/strong&gt; を活用して、ゲームロジックを独立したパッケージ (&lt;code&gt;packages/core&lt;/code&gt;) として切り出し、React UI (&lt;code&gt;apps/client&lt;/code&gt;) から完全に分離する設計手法を解説します。また、外部 I/O や非決定的な処理（乱数など）を抽象化する &lt;strong&gt;Port / Adapter パターン&lt;/strong&gt;についても触れます。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;課題なぜロジックが-ui-に染み出すのか&#34;&gt;課題：なぜロジックが UI に染み出すのか？&lt;/h2&gt;
&lt;p&gt;多くのプロジェクトでは、気づかないうちにロジックが React コンポーネントや Hooks の中に漏れ出していきます。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-tsx&#34; data-lang=&#34;tsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 密結合な例
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PlayerStats&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; () &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; [&lt;span style=&#34;color:#a6e22e&#34;&gt;hp&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;setHp&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;useState&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handleAttack&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; () &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// UI の中で計算ロジックが動いている
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;damage&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;floor&lt;/span&gt;(Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;random&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setHp&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;prev&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;max&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;prev&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;damage&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onClick&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;handleAttack&lt;/span&gt;}&amp;gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;攻撃を受ける&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;このような設計には以下の課題があります：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;テストの困難さ&lt;/strong&gt;: &lt;code&gt;Math.random()&lt;/code&gt; が直接使われているため、結果が不安定でユニットテストが書きにくい。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;再利用性の欠如&lt;/strong&gt;: この「ダメージ計算ロジック」を、サーバーサイドや別の UI フレームワークで使い回すことができない。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依存の混入&lt;/strong&gt;: ロジックを動かすために React の実行環境（レンダリングサイクル）が必要になる。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;設計npm-workspaces-による物理的隔離&#34;&gt;設計：npm workspaces による物理的隔離&lt;/h2&gt;
&lt;p&gt;ロジックを「物理的に」隔離するために、以下の monorepo 構成を採用します。&lt;/p&gt;</description>
    </item>
    <item>
      <title>AI コーディングエージェントを飼い慣らす：最強の「AGENTS.md」の書き方</title>
      <link>/posts/2026-03-28-write-agents-rules/</link>
      <pubDate>Sat, 28 Mar 2026 10:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-write-agents-rules/</guid>
      <description>&lt;h1 id=&#34;ai-コーディングエージェントを飼い慣らす最強のagentsmdの書き方&#34;&gt;AI コーディングエージェントを飼い慣らす：最強の「AGENTS.md」の書き方&lt;/h1&gt;
&lt;p&gt;近年、Gemini、Claude、Aider といった「自律型 AI コーディングエージェント」が開発現場に浸透しつつあります。彼らは指示一つでファイル構成を理解し、コードを書き、テストを実行して修正まで行います。&lt;/p&gt;
&lt;p&gt;しかし、エージェントを使い始めた多くのエンジニアが直面するのが、**「AI エージェントの暴走」**です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;型定義を面倒くさがって &lt;code&gt;any&lt;/code&gt; を連発する&lt;/li&gt;
&lt;li&gt;プロジェクト独自のディレクトリ構造を無視して勝手に &lt;code&gt;utils/&lt;/code&gt; を作る&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vitest --watch&lt;/code&gt; などの終了しないコマンドを叩いてフリーズする&lt;/li&gt;
&lt;li&gt;指示していないリファクタリングを始めて関係ないファイルを壊す&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;これらの問題を防ぎ、エージェントを「熟練のペアプロ相手」に変える魔法のファイル、それが &lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt; です。本記事では、AI エージェントに守らせるべきルールを定義する &lt;code&gt;AGENTS.md&lt;/code&gt; の書き方と、その設計思想を徹底解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;なぜ-agentsmd-が必要なのか&#34;&gt;なぜ &lt;code&gt;AGENTS.md&lt;/code&gt; が必要なのか&lt;/h2&gt;
&lt;p&gt;AI エージェントは、人間が数年かけて培った「プロジェクトの阿吽の呼吸」を知りません。
エージェントに与えられるコンテキスト（ファイル内容や履歴）は有限であり、その中で彼らは「最も確率的に正しそうな推論」を行います。その結果、彼らはしばしば**「最も楽な道（＝技術的負債を生む道）」**を選んでしまいます。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; をプロジェクトのルートに配置する理由は、**「エージェントの推論空間に強制的な制約（ガードレール）を設けるため」**です。&lt;/p&gt;
&lt;h3 id=&#34;なぜその設計にしたか外部メモリとしての役割&#34;&gt;なぜその設計にしたか：外部メモリとしての役割&lt;/h3&gt;
&lt;p&gt;エージェントは毎回ゼロベースで思考するわけではありませんが、多くのファイルを見すぎることで逆に重要なルールを見失う（ロスト・イン・ザ・ミドル現象）ことがあります。&lt;code&gt;AGENTS.md&lt;/code&gt; という明確なルールブックを定義し、エージェントの起動時やタスク開始時に必ず読み込ませることで、思考のブレを最小限に抑えることができます。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;agentsmd-に書くべき内容の-4-つの分類&#34;&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; に書くべき内容の 4 つの分類&lt;/h2&gt;
&lt;p&gt;効果的な &lt;code&gt;AGENTS.md&lt;/code&gt; は、以下の 4 つのセクションで構成するのがベストプラティスです。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;禁止事項 (Prohibitions):&lt;/strong&gt; 致命的なエラーや環境のハングを防ぐ&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命名規則・コーディング基準 (Standards):&lt;/strong&gt; コードの品質を一定に保つ&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;アーキテクチャ原則 (Architecture):&lt;/strong&gt; システムの整合性を維持する&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;テスト・検証方針 (Testing):&lt;/strong&gt; 修正の正しさを担保する&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;それぞれのセクションについて、具体的な記述例を見ていきましょう。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-禁止事項-prohibitions&#34;&gt;1. 禁止事項 (Prohibitions)&lt;/h2&gt;
&lt;p&gt;エージェントが最もやりがちな「環境破壊」を防ぐための最重要セクションです。&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;ルール&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;理由（なぜその設計にするか）&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;インタラクティブコマンドの禁止&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;npm init&lt;/code&gt; や &lt;code&gt;git commit&lt;/code&gt; (メッセージ入力待ち) など、ユーザー入力を待つコマンドは CLI エージェントをフリーズさせます。&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Watch Mode の禁止&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;vitest --watch&lt;/code&gt; 等はプロセスが終了しないため、エージェントが「完了」を検知できなくなります。&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;&lt;code&gt;any&lt;/code&gt; 型の原則禁止&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;AI は型の整合性を取るのが面倒になると &lt;code&gt;any&lt;/code&gt; で逃げようとします。これは長期的な保守性を著しく低下させます。&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;勝手な依存関係の追加禁止&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;package.json&lt;/code&gt; を書き換えて新しいライブラリを入れる行為は、セキュリティやビルドサイズに影響するため、人間の許可を必須にします。&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;実例コード&#34;&gt;実例コード&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## 禁止事項
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; **コマンド実行**:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`vi`&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;`nano`&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;`top`&lt;/span&gt; などのインタラクティブなコマンドは実行しない。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`npm start`&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;`vitest --watch`&lt;/span&gt; などの終了しないプロセスは背景実行 (&lt;span style=&#34;color:#e6db74&#34;&gt;`&amp;amp;`&lt;/span&gt;) するか、単発実行モードを使用すること。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; **TypeScript**:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`any`&lt;/span&gt; 型の使用は厳禁。どうしても必要な場合はコメントで理由を明記すること。
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; **Git**:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; ユーザーの明示的な指示なしに &lt;span style=&#34;color:#e6db74&#34;&gt;`git commit`&lt;/span&gt; や &lt;span style=&#34;color:#e6db74&#34;&gt;`git push`&lt;/span&gt; を行わない。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;2-命名規則コーディング基準-standards&#34;&gt;2. 命名規則・コーディング基準 (Standards)&lt;/h2&gt;
&lt;p&gt;プロジェクトの「見た目」と「一貫性」を守るためのルールです。ここを疎かにすると、エージェントは自分の得意なスタイル（多くの場合、学習データで最も多いスタイル）で書き始めてしまいます。&lt;/p&gt;</description>
    </item>
    <item>
      <title>EmacsでGoのLsp補完が死んだときの調査記録</title>
      <link>/posts/2026-03-21-emacs-gopls-failed-loop/</link>
      <pubDate>Sun, 22 Mar 2026 00:00:00 +0900</pubDate>
      <guid>/posts/2026-03-21-emacs-gopls-failed-loop/</guid>
      <description>&lt;h2 id=&#34;症状&#34;&gt;症状&lt;/h2&gt;
&lt;p&gt;Emacsで &lt;code&gt;.go&lt;/code&gt; ファイルを開くと以下のログが無限ループする。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[eglot] Asking EGLOT (mydns/(go-ts-mode go-mod-ts-mode)) politely to terminate
[jsonrpc] Server exited with status 2
[eglot] Reconnected!
[eglot] Connected! Server &amp;#39;gopls&amp;#39; now managing &amp;#39;(go-ts-mode go-mod-ts-mode)&amp;#39; buffers in project &amp;#39;mydns&amp;#39;.
[jsonrpc] (warning) Sentinel for EGLOT (...) still hasn&amp;#39;t run, deleting it!
[jsonrpc] Server exited with status 9
[eglot] Reconnected! [2 times]
Error running timer: (error &amp;#34;Selecting deleted buffer&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;status 9&lt;/code&gt; は SIGKILL。gopls が起動→即死→reconnect を繰り返し、補完が一切効かない状態。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fmt.&lt;/code&gt; と打っても候補が出ない、もしくは関係のないゴミ候補が出る。手動で &lt;code&gt;M-x completion-at-point&lt;/code&gt; を叩いても &lt;code&gt;No match&lt;/code&gt;。&lt;/p&gt;</description>
    </item>
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その6</title>
      <link>/posts/2026-03-05-test-driven-design-readmeter-6/</link>
      <pubDate>Thu, 05 Mar 2026 23:00:00 +0900</pubDate>
      <guid>/posts/2026-03-05-test-driven-design-readmeter-6/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;Part5でUIまで実装が終わった。機能として動くものはできた。テストも15件、全パス。&lt;/p&gt;
&lt;p&gt;ここで一度立ち止まって、&lt;strong&gt;なぜこの設計になったのか&lt;/strong&gt;を振り返る。&lt;/p&gt;
&lt;p&gt;正直に言う。最初の要件はこうだった。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「クリーンアーキテクチャっぽく、テスタブルにしたい」&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;それだけだった。クリーンアーキテクチャを理解して設計したわけじゃない。
&lt;strong&gt;結果的にクリーンアーキテクチャを遂行したのは生成AIだ。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;そしてPart3以降、自分はほとんど手を動かさなくなった。
設計ドキュメントをAIに渡したら、実装サイクルがほぼ自動で回るようになったからだ。&lt;/p&gt;
&lt;p&gt;途中でこう思った。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;「俺いる必要なくね？」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;この記事はその問いに向き合いながら、設計を改めて言語化したものだ。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;このシリーズ自体がtddだった&#34;&gt;このシリーズ自体がTDDだった&lt;/h2&gt;
&lt;p&gt;書き終えてから気づいたことがある。&lt;/p&gt;
&lt;p&gt;このシリーズのサイクルはこうだった。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;AIに実装させる
  ↓
動かす・読む・会話する（認識のズレを検出）
  ↓
ズレを言語化してAIにフィードバック
  ↓
納得したら記事にする
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ソフトウェアのTDDは「Red → Green → Refactor」だけど、
自分がやっていたのはこれだ。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Red&lt;/strong&gt; → 認識がズレていると感じる&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Green&lt;/strong&gt; → 会話して納得する&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactor&lt;/strong&gt; → 記事として言語化する&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;人間がテストケースになっていた。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TDDが「仕様をテストで表現する」なら、自分がやっていたのは「理解をフィードバックループで検証する」だ。構造は同じだと思う。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;誤っていた認識たち&#34;&gt;誤っていた認識たち&lt;/h2&gt;
&lt;p&gt;振り返ると、理解がズレていた箇所がいくつかあった。&lt;/p&gt;
&lt;h3 id=&#34;policyはvoと1-1になると思っていた&#34;&gt;「PolicyはVOと1-1になる」と思っていた&lt;/h3&gt;
&lt;p&gt;最初、&lt;code&gt;ReadingStatusPolicy&lt;/code&gt;が&lt;code&gt;ReadingStatus&lt;/code&gt;に対応しているのを見て、PolicyはVOと対になるものだと思っていた。&lt;/p&gt;
&lt;p&gt;違う。今回たまたま1-1になっているだけだ。&lt;/p&gt;
&lt;p&gt;Policyの本質は&lt;strong&gt;複数のEntityやVOをまたいだ条件判定&lt;/strong&gt;だ。たとえば「同じ本に同じユーザーが2回レビューできない」というルールは、Book・User・Review[]をまたぐ。これをPolicyに出す。&lt;/p&gt;
&lt;p&gt;VOに閉じるルールならVOに書けばいい。Entityをまたぐ判定が必要になったときにPolicyの出番だ。&lt;/p&gt;
&lt;h3 id=&#34;domainは外部を知らないという捉え方が逆だった&#34;&gt;「Domainは外部を知らない」という捉え方が逆だった&lt;/h3&gt;
&lt;p&gt;「DomainはDBを知らない」という言い方をよくするが、正確には逆だ。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;外部がDomainを知っている。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;向きの問題だ。Domainが何かを避けているのではなく、依存の矢印がすべてDomainに向かって刺さっている。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Route Handler
  ↓
Service
  ↓
Repository interface
  ↓
Domain（Entity / ValueObject / Policy）
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;この向きがわかって初めて、Serviceの役割も見えた。&lt;/p&gt;
&lt;h3 id=&#34;serviceが何をするのかわかっていなかった&#34;&gt;Serviceが何をするのかわかっていなかった&lt;/h3&gt;
&lt;p&gt;依存の向きが腑に落ちるまで、Serviceが何者なのかずっと曖昧だった。&lt;/p&gt;
&lt;p&gt;答えはシンプルだった。&lt;strong&gt;フローだけ持つ接着剤。&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;startReading&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;Book&lt;/span&gt;&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;findById&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;);   &lt;span style=&#34;color:#75715e&#34;&gt;// 取得
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;NotFoundError&lt;/span&gt;(...);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;changeStatus&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ReadingStatus&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Reading&lt;/span&gt;);     &lt;span style=&#34;color:#75715e&#34;&gt;// Entityのルールに従う
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt;);                  &lt;span style=&#34;color:#75715e&#34;&gt;// 保存
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;book&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ServiceはDomainのルールを&lt;strong&gt;自分で判定しない&lt;/strong&gt;。
&lt;code&gt;canTransition&lt;/code&gt;を自分で呼ばない。&lt;code&gt;book.changeStatus()&lt;/code&gt;に委ねるだけだ。
判定はEntity・VO・Policyが持っている。Serviceはその結果を使ってフローを組む。&lt;/p&gt;</description>
    </item>
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その5</title>
      <link>/posts/2026-03-04-test-driven-design-readmeter-5/</link>
      <pubDate>Thu, 05 Mar 2026 01:30:00 +0900</pubDate>
      <guid>/posts/2026-03-04-test-driven-design-readmeter-5/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;./part4&#34;&gt;Part4&lt;/a&gt; でカスタム例外クラスと残りのエンドポイントを実装し、APIが完成した。&lt;/p&gt;
&lt;p&gt;Part5では &lt;code&gt;src/app/page.tsx&lt;/code&gt; に簡易UIを実装する。バックエンドのロジックやテストには一切触れない。
フロントエンドからAPIを叩いて動くものを作るだけだ。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;実装方針&#34;&gt;実装方針&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;page.tsx&lt;/code&gt; をClient Componentにする。&lt;/p&gt;
&lt;p&gt;Server Componentでfetchする方法もあるが、ボタン操作のたびにstateを更新して再描画する必要がある。
今回のUIは「ボタンを押す → APIを叩く → 一覧を再取得して画面を更新する」という流れが中心なので、
&lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt; で管理するClient Componentのほうがシンプルだ。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;use client&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ファイル先頭にこの1行を追加する。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;実装するui&#34;&gt;実装するUI&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本の追加フォーム（id / title / isbn）&lt;/li&gt;
&lt;li&gt;本棚（Unread / Reading / Completed のグループ表示）&lt;/li&gt;
&lt;li&gt;各本へのアクションボタン
&lt;ul&gt;
&lt;li&gt;Unread → 「読み始める」ボタン&lt;/li&gt;
&lt;li&gt;Reading → 評価入力（1〜5）＋「読了にする」ボタン&lt;/li&gt;
&lt;li&gt;全ステータス → 削除ボタン&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;型定義&#34;&gt;型定義&lt;/h2&gt;
&lt;p&gt;APIレスポンスの型を定義する。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BookStatus&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Unread&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Reading&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Completed&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Book&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;isbn&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;BookStatus&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;rating&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;バックエンドの &lt;code&gt;ReadingStatus&lt;/code&gt; は &lt;code&gt;as const&lt;/code&gt; で定義した文字列リテラルなので、そのまま使える。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;データ取得&#34;&gt;データ取得&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetchBooks() {&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/api/books&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;取得失敗&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Book&lt;/span&gt;[] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setBooks&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setError&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;instanceof&lt;/span&gt; Error &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;不明なエラー&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } &lt;span style=&#34;color:#66d9ef&#34;&gt;finally&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setLoading&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;useEffect&lt;/span&gt;(() &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;fetchBooks&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}, []);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;fetchBooks&lt;/code&gt; は追加・ステータス変更・削除の後にも呼ぶ。
サーバーの状態を正として再取得するシンプルな方針だ。
楽観的更新（Optimistic Update）は今回やらない。&lt;/p&gt;</description>
    </item>
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その4</title>
      <link>/posts/2026-03-04-test-driven-design-readmeter-4/</link>
      <pubDate>Thu, 05 Mar 2026 00:50:00 +0900</pubDate>
      <guid>/posts/2026-03-04-test-driven-design-readmeter-4/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;./part1&#34;&gt;Part1&lt;/a&gt; でValueObject・Policy・Entityを作り、&lt;a href=&#34;./part2&#34;&gt;Part2&lt;/a&gt; でServiceをDI＋Mockテストで固めた。
&lt;a href=&#34;./part3&#34;&gt;Part3&lt;/a&gt; でPrismaを繋ぎ、&lt;code&gt;GET /api/books&lt;/code&gt; と &lt;code&gt;POST /api/books&lt;/code&gt; を動かした。&lt;/p&gt;
&lt;p&gt;Part4では残りのエンドポイントを実装する。やることは3つ。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;カスタム例外クラスの導入&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PATCH /api/books/:id/start&lt;/code&gt; と &lt;code&gt;PATCH /api/books/:id/complete&lt;/code&gt; の実装&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /api/books/:id&lt;/code&gt; の実装&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;そして「&lt;strong&gt;エラー種別ごとにHTTPステータスを整理する&lt;/strong&gt;」という設計判断を掘り下げる。&lt;/p&gt;
&lt;p&gt;また、Part3でPrisma v7特有のセットアップをしたが、v6以前と何が変わったのかをここで整理しておく。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;0-prisma-v7で何が変わったか&#34;&gt;0. Prisma v7で何が変わったか&lt;/h2&gt;
&lt;p&gt;Part3でPrismaをセットアップしたとき、v6以前と比べていくつか「見慣れない書き方」が必要だった。
v7は破壊的変更が多く、ネット上のv6時代の記事を参考にすると詰まる箇所がある。
ここで整理しておく。&lt;/p&gt;
&lt;p&gt;参考: &lt;a href=&#34;https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7&#34;&gt;Upgrade to Prisma ORM 7 | Prisma Documentation&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;generator-の変更&#34;&gt;generator の変更&lt;/h3&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-prisma&#34; data-lang=&#34;prisma&#34;&gt;// ❌ v6以前
generator client {
  provider = &amp;#34;prisma-client-js&amp;#34;
}

// ✅ v7
generator client {
  provider = &amp;#34;prisma-client&amp;#34;
  output   = &amp;#34;../src/generated/prisma&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;v7では &lt;code&gt;prisma-client-js&lt;/code&gt; が廃止され &lt;code&gt;prisma-client&lt;/code&gt; に変わった。
Rustベースのエンジンを廃止しTypeScriptネイティブになったことに伴う変更だ。
また &lt;code&gt;output&lt;/code&gt; が必須になり、&lt;code&gt;node_modules&lt;/code&gt; への自動生成はなくなった。&lt;/p&gt;</description>
    </item>
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その3</title>
      <link>/posts/2026-03-04-test-driven-design-readmeter-3/</link>
      <pubDate>Wed, 04 Mar 2026 17:50:00 +0900</pubDate>
      <guid>/posts/2026-03-04-test-driven-design-readmeter-3/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;./part1&#34;&gt;Part1&lt;/a&gt; でValueObject・Policy・Entityを作り、&lt;a href=&#34;./part2&#34;&gt;Part2&lt;/a&gt; でServiceをDI＋Mockテストで固めた。
累計13テスト、全パスの状態だ。&lt;/p&gt;
&lt;p&gt;Part3ではいよいよDBを繋ぐ。やることは3つ。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Prismaセットアップ＋スキーマ定義&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PrismaBookRepository&lt;/code&gt; 実装（ドメインオブジェクトへの変換）&lt;/li&gt;
&lt;li&gt;Route HandlerでDIを組み立てる&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;そして最後に「&lt;strong&gt;テストを書かない層を意図的に決める&lt;/strong&gt;」という話をする。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-prismaセットアップ&#34;&gt;1. Prismaセットアップ&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install prisma @prisma/client
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npx prisma init --datasource-provider sqlite
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;prisma/schema.prisma&lt;/code&gt; に Bookモデルを定義する。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-prisma&#34; data-lang=&#34;prisma&#34;&gt;generator client {
  provider = &amp;#34;prisma-client-js&amp;#34;
}

datasource db {
  provider = &amp;#34;sqlite&amp;#34;
  url      = env(&amp;#34;DATABASE_URL&amp;#34;)
}

model Book {
  id     String  @id
  title  String
  isbn   String
  status String
  rating Int?
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;User&lt;/code&gt; と &lt;code&gt;Review&lt;/code&gt; はまだDBに持たない。Book の CRUD が動けば Part3 のゴール。&lt;/p&gt;
&lt;h3 id=&#34;なぜ-status-を-string-で持つのか&#34;&gt;なぜ &lt;code&gt;status&lt;/code&gt; を String で持つのか&lt;/h3&gt;
&lt;p&gt;Prismaは SQLiteで enum をネイティブサポートしていない。
そのため &lt;code&gt;status String&lt;/code&gt; で持ち、取り出し時に &lt;code&gt;as ReadingStatus&lt;/code&gt; でキャストする。&lt;/p&gt;</description>
    </item>
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その2</title>
      <link>/posts/2026-03-04-test-driven-design-readmeter-2/</link>
      <pubDate>Wed, 04 Mar 2026 02:00:00 +0900</pubDate>
      <guid>/posts/2026-03-04-test-driven-design-readmeter-2/</guid>
      <description>&lt;h2 id=&#34;前回のおさらい&#34;&gt;前回のおさらい&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;/posts/readmeter-part1&#34;&gt;その1&lt;/a&gt;では ValueObject・Policy・Entity を作った。ポイントは「フレームワークを一切使わないので、テストがただの関数呼び出しになる」という体験だった。&lt;/p&gt;
&lt;p&gt;今回はいよいよ Service 層に入る。&lt;strong&gt;ここが設計の核心だ。&lt;/strong&gt; 複数のクラスをまたぐフローを、DI（依存性の注入）を使って DB から切り離す。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;今回追加したもの&#34;&gt;今回追加したもの&lt;/h2&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;src/
  domain/
    entity/
      User.ts              # 追加
      Review.ts            # 追加
    valueobject/
      UserName.ts          # 追加
      ReviewComment.ts     # 追加
  repository/
    BookRepository.ts      # 追加（Interface）
  service/
    BookShelfService.ts    # 追加
&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2 id=&#34;user-と-review-を追加する&#34;&gt;User と Review を追加する&lt;/h2&gt;
&lt;p&gt;まず Entity の準備。今回は単純なので ValueObject から作る。&lt;/p&gt;
&lt;h3 id=&#34;username&#34;&gt;UserName&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;UserName&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;constructor&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;readonly&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;trim&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;===&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;UserNameは空にできない&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;UserNameは50文字以内&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;trim()&lt;/code&gt; してから空チェックしているのがポイント。スペースだけのユーザー名を弾く。&lt;/p&gt;
&lt;h3 id=&#34;reviewcomment&#34;&gt;ReviewComment&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ReviewComment&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;constructor&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;readonly&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;trim&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;===&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ReviewCommentは空にできない&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ReviewCommentは1000文字以内&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;user-entity&#34;&gt;User Entity&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;UserName&lt;/span&gt; } &lt;span style=&#34;color:#66d9ef&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;../valueobject/UserName&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;User&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;constructor&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;readonly&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;readonly&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;UserName&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ) {}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;User&lt;/code&gt; 自体はシンプルだ。ロジックがない。ビジネスルールが増えれば &lt;code&gt;changeXxx()&lt;/code&gt; メソッドが生える設計だが、今は id と name を持つだけでいい。&lt;/p&gt;</description>
    </item>
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その1</title>
      <link>/posts/2026-03-04-test-driven-design-readmeter-1/</link>
      <pubDate>Wed, 04 Mar 2026 01:00:00 +0900</pubDate>
      <guid>/posts/2026-03-04-test-driven-design-readmeter-1/</guid>
      <description>&lt;h2 id=&#34;なぜこの記事を書いたか&#34;&gt;なぜこの記事を書いたか&lt;/h2&gt;
&lt;p&gt;動くものをまず作って、そこにテストを後付けしようとした。しかし、できなかった。&lt;/p&gt;
&lt;p&gt;具体的にはこういう壁にぶつかった。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ModelなどのDB接続の切り替えがうまくいかない&lt;/li&gt;
&lt;li&gt;Controllerをテストしようにもいろいろおかしくなる&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;悩んだ結果、気づいたことがある。&lt;strong&gt;そもそもそんなものは単体テストじゃない。&lt;/strong&gt; ロジックをControllerやModelに全部書いていたのが悪かった。&lt;/p&gt;
&lt;p&gt;「じゃあ最初からそう書けばいいじゃないか」という話だが、それが難しい。雑魚プログラマにいきなりクリーンな設計はできない。&lt;/p&gt;
&lt;p&gt;なので発想を逆にした。&lt;strong&gt;テスト前提でコードを書く。&lt;/strong&gt; テストが書けない場所にはロジックを書かない。それを体で覚えるために、今回のハンズオンを始めた。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;方針&#34;&gt;方針&lt;/h2&gt;
&lt;p&gt;生成AIに実装ヒントと次のステップを教えてもらいながら、コードは自分で書く。&lt;/p&gt;
&lt;p&gt;正直、コード自体はAIに書かせてもいいと思っている。ただ、設計の考え方は頭に入れる必要があるので、写経しながら理解を深めている。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;テストする場所の原則はシンプルだ。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;フレームワークで用意された便利クラスはテストしない&lt;/li&gt;
&lt;li&gt;DBやAPIなど外界と接する場所はテストしない&lt;/li&gt;
&lt;li&gt;自分で書いた純粋なTS部分だけをテストする&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;作るもの&#34;&gt;作るもの&lt;/h2&gt;
&lt;p&gt;読書レビューサイト。Todoアプリはビジネスロジックがほぼ存在しないのでテストの練習に向いていない。読書レビューサイトなら本の状態遷移（積読→読中→読了）などのルールが自然に生まれるので、テストの旨味がある。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技術スタック&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js&lt;/li&gt;
&lt;li&gt;SQLite + Prisma&lt;/li&gt;
&lt;li&gt;vitest&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;ディレクトリ構成&#34;&gt;ディレクトリ構成&lt;/h2&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;src/
  domain/
    entity/
    valueobject/
    policy/
  service/
  repository/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;この構成の考え方は以下の通り。&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;層&lt;/th&gt;
          &lt;th&gt;役割&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;entity&lt;/td&gt;
          &lt;td&gt;概念そのもの（Book, User）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;valueobject&lt;/td&gt;
          &lt;td&gt;値の制約を持つクラス（ISBN, Rating）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;policy&lt;/td&gt;
          &lt;td&gt;ビジネスルールを切り出したクラス&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;service&lt;/td&gt;
          &lt;td&gt;複数クラスをまたぐフロー&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;repository&lt;/td&gt;
          &lt;td&gt;DBとのアダプタ（副作用をここに閉じ込める）&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;repositoryはCI4でいうModelに近い立ち位置だ。&lt;/strong&gt; ただし決定的な違いがある。CI4のModelはActiveRecordパターンでクラス自身がDBを知っているため切り離せないが、repositoryはInterfaceと実装を分けることで差し替え可能にする。これがDI（依存性の注入）の肝で、テスト時にMockに差し替えられる。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;vitestのセットアップ&#34;&gt;vitestのセットアップ&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install -D vitest @vitejs/plugin-react vite-tsconfig-paths
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install -D @testing-library/react @testing-library/jest-dom
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;vitest.config.ts&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;defineConfig&lt;/span&gt; } &lt;span style=&#34;color:#66d9ef&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;vitest/config&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;react&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;@vitejs/plugin-react&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;tsconfigPaths&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;vite-tsconfig-paths&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;default&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;defineConfig&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;plugins&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [&lt;span style=&#34;color:#a6e22e&#34;&gt;react&lt;/span&gt;(), &lt;span style=&#34;color:#a6e22e&#34;&gt;tsconfigPaths&lt;/span&gt;()],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;test&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;environment&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;jsdom&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;globals&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;setupFiles&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;./src/test/setup.ts&amp;#39;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt;のscriptsに追加。&lt;/p&gt;</description>
    </item>
    <item>
      <title>AVL木と赤黒木をPythonで実装して比較する</title>
      <link>/posts/2026-03-01-avl-red-black-tree-impl/</link>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0900</pubDate>
      <guid>/posts/2026-03-01-avl-red-black-tree-impl/</guid>
      <description>AVL木と赤黒木をPythonで実装し、回転・バランス戦略の違いをベンチマークで比較する</description>
    </item>
    <item>
      <title>LinuxのCFSとEEVDFを整理する - スケジューラはなぜ赤黒木を使うのか</title>
      <link>/posts/2026-03-01-linux-scheduler/</link>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0900</pubDate>
      <guid>/posts/2026-03-01-linux-scheduler/</guid>
      <description>CFSのvruntime・赤黒木・タイムスライスの仕組みと、Linux 6.6で導入されたEEVDFへの変遷をSRE視点で整理する</description>
    </item>
    <item>
      <title>Linuxの起動フローを整理する - UEFI/BIOSからinitまで</title>
      <link>/posts/2026-02-28-linux-startup-flow/</link>
      <pubDate>Sat, 28 Feb 2026 00:00:00 +0900</pubDate>
      <guid>/posts/2026-02-28-linux-startup-flow/</guid>
      <description>UEFI/BIOSからブートローダー、カーネル、initまでのLinux起動フローをSRE視点で整理する</description>
    </item>
    <item>
      <title>歴史地図アプリを雑にk3sへデプロイした</title>
      <link>/posts/2026-02-25-k3s-history-map-deploy/</link>
      <pubDate>Wed, 25 Feb 2026 22:00:00 +0900</pubDate>
      <guid>/posts/2026-02-25-k3s-history-map-deploy/</guid>
      <description>React&#43;Vite&#43;MapLibreで作った歴史地図アプリをk3sにデプロイした話。hostPathマウント、Viteの罠、ngrokとTailscaleを駆使したファイル転送など泥臭い記録。</description>
    </item>
    <item>
      <title>WSL2でExpo &#43; E2Eテスト（MaestroとDetox）を試みて完全に詰んだ話</title>
      <link>/posts/2026-02-23-fxxk-mobile-e2e-test/</link>
      <pubDate>Mon, 23 Feb 2026 21:30:00 +0900</pubDate>
      <guid>/posts/2026-02-23-fxxk-mobile-e2e-test/</guid>
      <description>&lt;h2 id=&#34;tldr&#34;&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;WSL2環境でExpo（React Native）のE2EテストをMaestroとDetoxで試みたが、どちらもWSL2とWindowsエミュレータの構造的な問題で動かなかった。&lt;/p&gt;
&lt;p&gt;かなり過言ではあるが、あえて感情的になるならば、Mobile開発においてMac以外は人権がない。というかあまりにもMac環境以外がだるすぎる。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;環境&#34;&gt;環境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OS: Windows + WSL2（Ubuntu）&lt;/li&gt;
&lt;li&gt;Expo SDK 54 / React Native 0.81.5&lt;/li&gt;
&lt;li&gt;New Architecture有効&lt;/li&gt;
&lt;li&gt;Androidエミュレータ: Windows側で動作（Medium Phone API 36）&lt;/li&gt;
&lt;li&gt;ADB: Windows側のものをWSL2から参照&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;maestroを試みる&#34;&gt;Maestroを試みる&lt;/h2&gt;
&lt;h3 id=&#34;インストール&#34;&gt;インストール&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;curl -Ls &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://get.maestro.mobile.dev&amp;#34;&lt;/span&gt; | bash
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;export PATH&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$HOME&lt;span style=&#34;color:#e6db74&#34;&gt;/.maestro/bin:&lt;/span&gt;$PATH&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ここで最初の罠。&lt;code&gt;maestro --help&lt;/code&gt;を叩くとAI系の全く別のCLIツールが応答した。同名の別アプリが先にPATHに入っていたため。&lt;code&gt;$HOME/.maestro/bin&lt;/code&gt;をPATHの&lt;strong&gt;先頭&lt;/strong&gt;に置くことで解決。&lt;/p&gt;
&lt;h3 id=&#34;フローの準備&#34;&gt;フローの準備&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# .maestro/add_and_complete_task.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;appId&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;com.example.myapp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#ae81ff&#34;&gt;launchApp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;tapOn&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;text&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;追加&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;inputText&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;テストタスク&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;tapOn&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;text&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;追加する&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;assertVisible&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;text&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;NOW&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;実行して即死&#34;&gt;実行して即死&lt;/h3&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;You have 0 devices connected, which is not enough to run 1 shards.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;エミュレータはWindows側で動いており、&lt;code&gt;adb devices&lt;/code&gt;には&lt;code&gt;emulator-5554&lt;/code&gt;が見えている。しかしMaestroはWSL2側でデバイスを探すため認識できない。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--udid=emulator-5554&lt;/code&gt;を指定しても：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Device emulator-5554 was requested, but it is not connected.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;maestro start-device --platform=android&lt;/code&gt;を試みると：&lt;/p&gt;</description>
    </item>
    <item>
      <title>react-native-pdf 6.7.7のiOS表示問題をpatch-packageで解決する</title>
      <link>/posts/2026-02-17-expo-react-native-pdf-patch/</link>
      <pubDate>Tue, 17 Feb 2026 14:30:00 +0900</pubDate>
      <guid>/posts/2026-02-17-expo-react-native-pdf-patch/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;業務で&lt;code&gt;react-native-pdf&lt;/code&gt;を使用した際、AndroidではPDFが正常に表示されるのにiOSでは表示されないという問題に遭遇しました。&lt;/p&gt;
&lt;p&gt;この記事では、GitHubのissueで共有された解決策である&lt;code&gt;patch-package&lt;/code&gt;を使ったパッチ適用方法について解説します。&lt;/p&gt;
&lt;h2 id=&#34;問題の概要&#34;&gt;問題の概要&lt;/h2&gt;
&lt;h3 id=&#34;環境&#34;&gt;環境&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;react-native-pdf&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;^6.7.7&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;react-native&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0.80.1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;react-native-blob-util&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;^0.22.2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;症状&#34;&gt;症状&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Android: PDF表示が正常に動作&lt;/li&gt;
&lt;li&gt;iOS: PDFが表示されない&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;この問題は、React Native 0.80以降で&lt;code&gt;react-native-pdf&lt;/code&gt;を使用した際に発生することが確認されています。&lt;/p&gt;
&lt;p&gt;参考: &lt;a href=&#34;https://github.com/wonday/react-native-pdf/issues/966&#34;&gt;pdf is not displayed，Android is working fine, but there are problems with iOS #966&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;解決策-patch-packageを使う&#34;&gt;解決策: patch-packageを使う&lt;/h2&gt;
&lt;p&gt;GitHubのissueで&lt;a href=&#34;https://github.com/wonday/react-native-pdf/issues/966&#34;&gt;@anhnguyen123&lt;/a&gt;さんが共有してくれたパッチファイルを適用することで、この問題を解決できます。&lt;/p&gt;
&lt;h3 id=&#34;1-patch-packageのインストール&#34;&gt;1. patch-packageのインストール&lt;/h3&gt;
&lt;p&gt;まず、&lt;code&gt;patch-package&lt;/code&gt;と&lt;code&gt;postinstall-postinstall&lt;/code&gt;をdevDependenciesとしてインストールします。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# npmの場合&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install --save-dev patch-package
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# yarnの場合&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yarn add --dev patch-package postinstall-postinstall
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;参考: &lt;a href=&#34;https://www.npmjs.com/package/patch-package&#34;&gt;patch-package - npm&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;2-packagejsonにpostinstallスクリプトを追加&#34;&gt;2. package.jsonにpostinstallスクリプトを追加&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt;の&lt;code&gt;scripts&lt;/code&gt;セクションに、&lt;code&gt;postinstall&lt;/code&gt;スクリプトを追加します。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;postinstall&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;patch-package&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;このスクリプトにより、&lt;code&gt;npm install&lt;/code&gt;または&lt;code&gt;yarn install&lt;/code&gt;を実行するたびに、自動的にパッチが適用されます。&lt;/p&gt;
&lt;h3 id=&#34;3-パッチファイルの配置&#34;&gt;3. パッチファイルの配置&lt;/h3&gt;
&lt;p&gt;GitHubのissueからパッチファイル&lt;code&gt;react-native-pdf+6.7.7.patch&lt;/code&gt;をダウンロードし、プロジェクトルートに&lt;code&gt;patches&lt;/code&gt;ディレクトリを作成してそこに配置します。&lt;/p&gt;</description>
    </item>
    <item>
      <title>歴史地図アプリに日本語検索を実装: GeoJSONデータの効率的な翻訳手法</title>
      <link>/posts/2026-02-15-generate-data-from-gemini/</link>
      <pubDate>Sun, 15 Feb 2026 19:00:00 +0900</pubDate>
      <guid>/posts/2026-02-15-generate-data-from-gemini/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;歴史的国境を可視化する地図アプリを作っていたら、「日本語で国名検索ができない」という問題に直面した。外部のGeoJSONデータは英語のみで、日本語プロパティがない。&lt;/p&gt;
&lt;p&gt;そこで、&lt;strong&gt;Gemini APIを使って効率的にデータを翻訳し、日本語検索を実装した&lt;/strong&gt;手法を紹介する。&lt;/p&gt;
&lt;h2 id=&#34;問題-外部geojsonデータには日本語がない&#34;&gt;問題: 外部GeoJSONデータには日本語がない&lt;/h2&gt;
&lt;p&gt;使用したデータソース:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;現代国境&lt;/strong&gt;: &lt;a href=&#34;https://www.naturalearthdata.com/&#34;&gt;Natural Earth&lt;/a&gt; (約200カ国)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;歴史的国境&lt;/strong&gt;: &lt;a href=&#34;https://github.com/aourednik/historical-basemaps&#34;&gt;aourednik/historical-basemaps&lt;/a&gt; (18ファイル、紀元前2000年〜1920年)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Feature&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;properties&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;NAME&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;France&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;NAME_JA&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// ← 日本語プロパティがない!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;geometry&amp;#34;&lt;/span&gt;: { &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;...&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;このままでは「フランス」で検索できない。&lt;/p&gt;
&lt;h2 id=&#34;解決策-翻訳キャッシュを使った効率的なデータ拡張&#34;&gt;解決策: 翻訳キャッシュを使った効率的なデータ拡張&lt;/h2&gt;
&lt;h3 id=&#34;アプローチ1-愚直な方法-非効率&#34;&gt;アプローチ1: 愚直な方法 (非効率)&lt;/h3&gt;
&lt;p&gt;各ファイルごとに全データをLLMに投げる:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ❌ 非効率: 同じ国名を何度も翻訳
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;of&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;geoJsonFiles&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;translated&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;translateAll&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;); &lt;span style=&#34;color:#75715e&#34;&gt;// Franceを18回翻訳...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;translated&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;問題点:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同じ国名が複数ファイルに登場 → 重複翻訳&lt;/li&gt;
&lt;li&gt;トークン消費が膨大&lt;/li&gt;
&lt;li&gt;処理時間が長い&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;アプローチ2-翻訳キャッシュ方式-効率的-&#34;&gt;アプローチ2: 翻訳キャッシュ方式 (効率的) ✅&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;全ファイル共通の翻訳キャッシュを使い回す:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ✅ 効率的: 一度翻訳した国名は二度と翻訳しない
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;translationCache&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {}; &lt;span style=&#34;color:#75715e&#34;&gt;// { &amp;#34;France&amp;#34;: &amp;#34;フランス&amp;#34;, ... }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;of&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;geoJsonFiles&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;file&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// 未翻訳の国名のみ抽出
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;newNames&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;extractUntranslatedNames&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;translationCache&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// 新規の国名だけ翻訳
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;newNames&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;translations&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;translate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;newNames&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Object.&lt;span style=&#34;color:#a6e22e&#34;&gt;assign&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;translationCache&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;translations&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// キャッシュを使って適用
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;applyTranslations&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;translationCache&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;実装-nodejsスクリプト&#34;&gt;実装: Node.jsスクリプト&lt;/h2&gt;
&lt;h3 id=&#34;完全なコード&#34;&gt;完全なコード&lt;/h3&gt;
&lt;p&gt;この手法をNode.jsスクリプトとして実装した。Gemini 2.5 Flash Liteを使用している。この程度の翻訳ならこれで十分。&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQLのCTEが現場で少ない理由を実務経験から考える</title>
      <link>/posts/2026-02-12-pgsql-cte/</link>
      <pubDate>Thu, 12 Feb 2026 10:50:00 +0900</pubDate>
      <guid>/posts/2026-02-12-pgsql-cte/</guid>
      <description>バッチ処理でCTEを使った実務経験から、CTEの強み・弱み、PostgreSQL 12での最適化の進化、サブクエリ・一時テーブルとの使い分けを解説</description>
    </item>
    <item>
      <title>AsyncStorageって裏側何やってんの？ - 2.0と3.0の実装の違いを調べてみた</title>
      <link>/posts/2026-02-08-async-storage/</link>
      <pubDate>Sun, 08 Feb 2026 17:50:00 +0900</pubDate>
      <guid>/posts/2026-02-08-async-storage/</guid>
      <description>&lt;p&gt;私は普段React NativeでExpo触ってるので、AsyncStorageはよく使うんだけど、「そういえばAsyncStorageって裏側何やってんだろう？」って疑問が湧いてきたので調べてみることにした。&lt;/p&gt;
&lt;h2 id=&#34;asyncstorageの裏側&#34;&gt;AsyncStorageの裏側&lt;/h2&gt;
&lt;p&gt;AsyncStorageのバージョンによって実装が少し違う。&lt;/p&gt;
&lt;h3 id=&#34;asyncstorage-20の実装&#34;&gt;AsyncStorage 2.0の実装&lt;/h3&gt;
&lt;p&gt;iOS/Androidのみ調査。&lt;/p&gt;
&lt;p&gt;公式ドキュメント: &lt;a href=&#34;https://react-native-async-storage.github.io/2.0/advanced/Where-data-stored/&#34;&gt;Where your data is stored - Async Storage&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&#34;ios-20&#34;&gt;iOS (2.0)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;manifest.json&lt;/code&gt;ファイルに保存される&lt;/li&gt;
&lt;li&gt;JSONファイル形式&lt;/li&gt;
&lt;li&gt;パス: &lt;code&gt;Documents/RCTAsyncLocalStorage_V1/manifest.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;詳細: 1024文字以下のデータは&lt;code&gt;manifest.json&lt;/code&gt;に、それより大きいデータは個別ファイル(MD5ハッシュ名)に保存される&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;android-20&#34;&gt;Android (2.0)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;SQLiteデータベースに保存される&lt;/li&gt;
&lt;li&gt;データベース名: &lt;code&gt;RKStorage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;パス: &lt;code&gt;/data/data/{package_name}/databases/RKStorage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;asyncstorage-30-nextの実装&#34;&gt;AsyncStorage 3.0 (next)の実装&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;公式ドキュメント&lt;/strong&gt;: &lt;a href=&#34;https://react-native-async-storage.github.io/3.0-next/&#34;&gt;https://react-native-async-storage.github.io/3.0-next/&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&#34;対応プラットフォーム&#34;&gt;対応プラットフォーム&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Android (SQLite)&lt;/li&gt;
&lt;li&gt;iOS (SQLite) ✨&lt;/li&gt;
&lt;li&gt;macOS (SQLite)&lt;/li&gt;
&lt;li&gt;visionOS (legacy fallback, single database only)&lt;/li&gt;
&lt;li&gt;Web (IndexedDB backend)&lt;/li&gt;
&lt;li&gt;Windows (legacy fallback, single database only)&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&#34;ios-30&#34;&gt;iOS (3.0)&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;SQLiteデータベースに変更された&lt;/li&gt;
&lt;li&gt;Androidと同じ実装に統一&lt;/li&gt;
&lt;li&gt;パフォーマンスと安定性が向上&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&#34;android-30&#34;&gt;Android (3.0)&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;引き続きSQLite&lt;/li&gt;
&lt;li&gt;より洗練された実装&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3.0からはiOSもAndroidも両方SQLiteになって、実装が統一されるそうだ。&lt;/p&gt;
&lt;h5 id=&#34;互換性&#34;&gt;互換性&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;React Native 0.76以降が必要(iOS/Android)&lt;/li&gt;
&lt;li&gt;Kotlin 2.1.0&lt;/li&gt;
&lt;li&gt;iOS minimum target: 13&lt;/li&gt;
&lt;li&gt;Android minimum SDK: 24&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;なぜiosでmanifestjsonからsqliteに変更したのか&#34;&gt;なぜiOSでmanifest.jsonからSQLiteに変更したのか&lt;/h2&gt;
&lt;p&gt;あくまでも推測ではあるがやってみた。&lt;/p&gt;</description>
    </item>
    <item>
      <title>React NativeのTodoアプリで実装する相対時間ベースのプリセット機能</title>
      <link>/posts/2026-02-08-expo-presets/</link>
      <pubDate>Sun, 08 Feb 2026 10:50:00 +0900</pubDate>
      <guid>/posts/2026-02-08-expo-presets/</guid>
      <description>絶対時間ではなく相対時間（dueHoursOffset）で期限を管理することで、繰り返しタスクを効率化するプリセット機能の実装方法を解説</description>
    </item>
    <item>
      <title>Emacsのdotfilesをモジュール化してメンテナンス性を向上させた話</title>
      <link>/posts/2026-02-06-emacs-refactor/</link>
      <pubDate>Fri, 06 Feb 2026 23:30:00 +0900</pubDate>
      <guid>/posts/2026-02-06-emacs-refactor/</guid>
      <description>900行の肥大化したEmacs設定をファイル分割し、requireとloadの違いを理解して安定した環境を構築</description>
    </item>
    <item>
      <title>三竦（さんすくみ）要件定義書</title>
      <link>/posts/2026-02-06-sansuku/</link>
      <pubDate>Fri, 06 Feb 2026 07:30:00 +0900</pubDate>
      <guid>/posts/2026-02-06-sansuku/</guid>
      <description>犬猿雉の三すくみを使った追跡型対戦ゲームの要件定義。リアルタイムアクション &#43; 戦略性を両立したシンプルなモバイルゲーム</description>
    </item>
    <item>
      <title>GEMINI.mdでAIに開発履歴を管理させる方法</title>
      <link>/posts/2026-01-31-gemini-cli-history/</link>
      <pubDate>Sat, 31 Jan 2026 19:30:00 +0900</pubDate>
      <guid>/posts/2026-01-31-gemini-cli-history/</guid>
      <description>GEMINI.mdファイルを使ってAI（Gemini）にコーディングルールと開発履歴を管理させる手法を紹介します。AIが自分で学習・改善していくドキュメント駆動開発の実践例。</description>
    </item>
    <item>
      <title>Gemini CLIでExpo Todoアプリを爆速開発した話</title>
      <link>/posts/2026-01-31-gemini-cli-expo/</link>
      <pubDate>Sat, 31 Jan 2026 19:00:00 +0900</pubDate>
      <guid>/posts/2026-01-31-gemini-cli-expo/</guid>
      <description>Gemini CLI を使って Expo の Todo アプリを開発した体験記。GEMINI.md でルールを管理し、段階的に機能を追加していく方法を紹介します。</description>
    </item>
    <item>
      <title>React NativeでTextInputの日本語入力が壊れる問題と解決方法</title>
      <link>/posts/2026-01-31-react-native-input-text-ime-bug/</link>
      <pubDate>Sat, 31 Jan 2026 18:30:00 +0900</pubDate>
      <guid>/posts/2026-01-31-react-native-input-text-ime-bug/</guid>
      <description>React NativeのTextInputで日本語入力時に変換候補が消える問題の原因と、autoComplete/autoCorrectによる解決方法を解説します。</description>
    </item>
    <item>
      <title>Proxmox LXCコンテナでJupyterLab環境構築 - 試行錯誤とトラブルシューティング</title>
      <link>/posts/2026-01-26-proxmox-jupyter/</link>
      <pubDate>Mon, 26 Jan 2026 18:30:00 +0900</pubDate>
      <guid>/posts/2026-01-26-proxmox-jupyter/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;Proxmox上にJupyterLabのLXC環境を構築しました。当初はGeminiに任せて試行錯誤しましたが、最終的にベストプラクティスに辿り着いたので、その過程と解決策をまとめます。&lt;/p&gt;
&lt;h2 id=&#34;構築の基本方針&#34;&gt;構築の基本方針&lt;/h2&gt;
&lt;p&gt;当初は「Root + グローバル環境」で構築しようとしましたが、最終的に**「専用ユーザー + 仮想環境（venv）」**による安全でクリーンな構成に落ち着きました。&lt;/p&gt;
&lt;h3 id=&#34;最終構成&#34;&gt;最終構成&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS&lt;/strong&gt;: Ubuntu 24.04 LTS (LXC Container)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ユーザー&lt;/strong&gt;: &lt;code&gt;jupyter&lt;/code&gt; (非Root運用)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jupyter&lt;/strong&gt;: JupyterLab (v4.x)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;環境&lt;/strong&gt;: &lt;code&gt;/opt/jupyter/venv&lt;/code&gt; (OSと分離した仮想環境)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;環境構築手順&#34;&gt;環境構築手順&lt;/h2&gt;
&lt;h3 id=&#34;1-osの準備&#34;&gt;1. OSの準備&lt;/h3&gt;
&lt;p&gt;Ubuntu 24.04の最小構成に必要なパッケージをインストールします。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;apt update &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;apt install -y python3-full build-essential
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;python3-full&lt;/code&gt;が重要です。これがないと後述するPEP 668の問題に直面します。&lt;/p&gt;
&lt;h3 id=&#34;2-専用ユーザーとディレクトリの作成&#34;&gt;2. 専用ユーザーとディレクトリの作成&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 専用ユーザー作成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;useradd -m -s /bin/bash jupyter
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Jupyter本体用のディレクトリ準備&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mkdir -p /opt/jupyter
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;chown jupyter:jupyter /opt/jupyter
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;3-仮想環境の構築&#34;&gt;3. 仮想環境の構築&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;jupyter&lt;/code&gt;ユーザーとして、OSの制限を受けない独立した環境を作ります。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;su - jupyter
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python3 -m venv /opt/jupyter/venv
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;source /opt/jupyter/venv/bin/activate
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# JupyterLabとカーネルのインストール&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install jupyterlab ipykernel pandas
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;4-systemdによるデーモン化&#34;&gt;4. systemdによるデーモン化&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/etc/systemd/system/jupyter.service&lt;/code&gt;を作成します。&lt;/p&gt;</description>
    </item>
    <item>
      <title>PythonとWorld Bankデータで世界の産業構造を可視化する方法</title>
      <link>/posts/2026-01-26-gdp-by-sector/</link>
      <pubDate>Mon, 26 Jan 2026 09:30:00 +0900</pubDate>
      <guid>/posts/2026-01-26-gdp-by-sector/</guid>
      <description>World BankのオープンデータとPythonを使って、世界各国の産業構造（第一次・第二次・第三次産業）の変化を可視化してみました。初心者でもわかる経済データ分析の入門記事です。</description>
    </item>
    <item>
      <title>ラズパイ6台で作る、絶対に止まらない最強の自宅ネットワーク冗長化計画</title>
      <link>/posts/2026-01-24-new-home-nw/</link>
      <pubDate>Sat, 24 Jan 2026 19:00:00 +0900</pubDate>
      <guid>/posts/2026-01-24-new-home-nw/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;「自宅サーバーを構築したが、1台落ちただけで家族全員のネットが止まった」
そんな苦い経験（特にDNS/DHCP周りでの同期失敗）を経て、今回は&lt;strong&gt;Raspberry Pi 6台（＋α）を駆使した「高可用性（HA）」に特化した自宅ネットワーク&lt;/strong&gt;を再設計します。&lt;/p&gt;
&lt;p&gt;今回のコンセプトは「速度よりも、止まらないこと」。
北欧神話の神々の名を冠した6台のラズパイによる、3層の冗長化レイヤーを構築します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-ネットワーク全体像&#34;&gt;1. ネットワーク全体像&lt;/h2&gt;
&lt;p&gt;上位ルーターとはWi-Fiで接続し、内部ネットワークは有線L2スイッチを中心に構成します。物理的に役割を分離することで、障害時の原因切り分けを容易にしています。&lt;/p&gt;
&lt;h3 id=&#34;階層化の設計案&#34;&gt;階層化の設計案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Edge層 (L1)&lt;/strong&gt;: インターネットへの門番。KeepalivedでゲートウェイIPを共有。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Core層 (L2)&lt;/strong&gt;: DHCPやDNS、認証など、NWの頭脳となる機能を同期。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service層 (L3)&lt;/strong&gt;: ファイルサーバーなどの実データをレプリケーションして保持。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-サーバー構成表北欧神話の神々&#34;&gt;2. サーバー構成表：北欧神話の神々&lt;/h2&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;物理筐体&lt;/th&gt;
          &lt;th&gt;ホスト名&lt;/th&gt;
          &lt;th&gt;役割&lt;/th&gt;
          &lt;th&gt;冗長化の仕組み&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Pi 3 (A)&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Odin&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;主系Gateway / VPN&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Keepalived (VIP: 192.168.1.1)&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Pi 3 (B)&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Frigg&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;副系Gateway / VPN&lt;/td&gt;
          &lt;td&gt;Odinと仮想IPを共有&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Pi 3 (C)&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Huginn&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;DNS / DHCP Primary&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;ISC-DHCP Failover / Gravity Sync&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Pi 3 (D)&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Muninn&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;DNS / DHCP Secondary&lt;/td&gt;
          &lt;td&gt;Huginnとリアルタイム同期&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Pi 3 (E)&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Mjolnir&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;ストレージ (NAS)&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;GlusterFS + Keepalived (VIP: .200)&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Pi 3 (F)&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Gungnir&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;ストレージ (NAS)&lt;/td&gt;
          &lt;td&gt;Mjolnirとデータレプリケーション&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;3-過去の失敗を防ぐ守りの技術選定&#34;&gt;3. 過去の失敗を防ぐ「守り」の技術選定&lt;/h2&gt;
&lt;h3 id=&#34;-dnsdhcp仮想ipに頼らない&#34;&gt;① DNS/DHCP：仮想IPに頼らない&lt;/h3&gt;
&lt;p&gt;過去、DNSやDHCPを仮想IP（Keepalived）で制御しようとして失敗した経験から、今回は&lt;strong&gt;プロトコル標準の冗長化&lt;/strong&gt;を採用します。&lt;/p&gt;</description>
    </item>
    <item>
      <title>データが暴く物価高騰の真実 - エネルギー価格と為替の相関分析で見えた意外な結論</title>
      <link>/posts/2026-01-23-study-stats-eco/</link>
      <pubDate>Fri, 23 Jan 2026 08:30:00 +0900</pubDate>
      <guid>/posts/2026-01-23-study-stats-eco/</guid>
      <description>「円安が物価高の原因」は本当か？消費者物価指数と為替レートの詳細分析から見えた、統計が示す意外な真実。相関係数0.100が語る、エネルギー価格変動の本当のドライバーとは。</description>
    </item>
    <item>
      <title>K3s実験環境構築マニュアル：安全に実験→破壊→復元のサイクルを回す</title>
      <link>/posts/2026-01-19-k8s-backup-restore/</link>
      <pubDate>Mon, 19 Jan 2026 10:30:00 +0900</pubDate>
      <guid>/posts/2026-01-19-k8s-backup-restore/</guid>
      <description>Proxmox上のK3s環境で安全に実験・破壊・復元サイクルを回すための完全ガイド</description>
    </item>
    <item>
      <title>LeetCode 735: Asteroid Collision - スタックで衝突判定を美しく解く</title>
      <link>/posts/2026-01-13-leetcode-735/</link>
      <pubDate>Tue, 13 Jan 2026 19:30:00 +0900</pubDate>
      <guid>/posts/2026-01-13-leetcode-735/</guid>
      <description>&lt;h2 id=&#34;問題概要&#34;&gt;問題概要&lt;/h2&gt;
&lt;p&gt;整数で表される小惑星の配列 &lt;code&gt;asteroids&lt;/code&gt; が与えられる。各小惑星について：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;絶対値：大きさ&lt;/li&gt;
&lt;li&gt;符号：方向（正=右、負=左）&lt;/li&gt;
&lt;li&gt;全て同じ速度で移動&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;衝突ルール：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小さい方が爆発&lt;/li&gt;
&lt;li&gt;同じ大きさなら両方爆発&lt;/li&gt;
&lt;li&gt;同じ方向に移動する小惑星は衝突しない&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;全ての衝突後の状態を返せ。&lt;/p&gt;
&lt;h2 id=&#34;失敗した実装&#34;&gt;失敗した実装&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; collections &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; deque
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Solution&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;asteroidCollision&lt;/span&gt;(self, asteroids: List[int]) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; List[int]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        stack &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; aster &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; asteroids:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; len(stack) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; (aster &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; (stack[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                stack&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(aster)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; len(stack) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; (aster &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; (stack[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; abs(aster) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; abs(stack[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    stack&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;pop()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; len(stack) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; (stack[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; (aster &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    stack&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(aster)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;elif&lt;/span&gt; abs(aster) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; abs(stack[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    stack&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;pop()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; stack
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;問題点&#34;&gt;問題点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;同じ条件判定 &lt;code&gt;if len(stack) &amp;lt;= 0 or (stack[-1] &amp;lt;= 0) == (aster &amp;lt;= 0)&lt;/code&gt; が2箇所に重複&lt;/li&gt;
&lt;li&gt;制御フローが複雑で読みにくい&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(aster &amp;lt;= 0) != (stack[-1] &amp;lt;= 0)&lt;/code&gt; は「符号が異なる」を検出するが、衝突しないケースも含む&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;最適解&#34;&gt;最適解&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Solution&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;asteroidCollision&lt;/span&gt;(self, asteroids: List[int]) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; List[int]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        stack &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; asteroid &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; asteroids:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; stack &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; asteroid &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; stack[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#75715e&#34;&gt;# 右向き vs 左向きの衝突が発生&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; abs(stack[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; abs(asteroid):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#75715e&#34;&gt;# 右向きが小さい → 爆発して次の右向きとも衝突判定&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    stack&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;pop()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#66d9ef&#34;&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;elif&lt;/span&gt; abs(stack[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; abs(asteroid):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#75715e&#34;&gt;# 同じ大きさ → 両方爆発&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    stack&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;pop()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#75715e&#34;&gt;# 衝突しなかった or 左向きが勝った&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                stack&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(asteroid)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; stack
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;わかったこと&#34;&gt;わかったこと&lt;/h2&gt;
&lt;h3 id=&#34;1-ループ条件の本質&#34;&gt;1. ループ条件の本質&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;asteroid &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; stack[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;これは &lt;code&gt;(asteroid &amp;lt; 0) and (0 &amp;lt; stack[-1])&lt;/code&gt; と同じ。つまり：&lt;/p&gt;</description>
    </item>
    <item>
      <title>Go言語でネットワークプログラミングを学ぶ - 第3章</title>
      <link>/posts/2026-01-11-go-network-3/</link>
      <pubDate>Sun, 11 Jan 2026 20:19:00 +0900</pubDate>
      <guid>/posts/2026-01-11-go-network-3/</guid>
      <description>スイッチとMACアドレスを実装し、複数ノードからなるローカルネットワークを構築する</description>
    </item>
    <item>
      <title>Go言語でネットワークプログラミングを学ぶ - 第2章</title>
      <link>/posts/2026-01-11-go-network-2/</link>
      <pubDate>Sun, 11 Jan 2026 10:00:00 +0900</pubDate>
      <guid>/posts/2026-01-11-go-network-2/</guid>
      <description>時間の概念とスループット制御をネットワークシミュレーションに導入する</description>
    </item>
    <item>
      <title>Go言語でネットワークプログラミングを学ぶ - 第1章</title>
      <link>/posts/2026-01-10-go-network-1/</link>
      <pubDate>Sat, 10 Jan 2026 18:00:00 +0900</pubDate>
      <guid>/posts/2026-01-10-go-network-1/</guid>
      <description>ネットワークの基本要素であるNode、Link、Packetを Go言語で実装する</description>
    </item>
    <item>
      <title>Go言語でネットワークプログラミングを学ぶ - 第0章</title>
      <link>/posts/2026-01-10-go-network/</link>
      <pubDate>Sat, 10 Jan 2026 17:50:00 +0900</pubDate>
      <guid>/posts/2026-01-10-go-network/</guid>
      <description>Go言語を使ってネットワークプログラミングを体系的に学ぶためのカリキュラム第0章</description>
    </item>
    <item>
      <title>本が読めなかったから、仕事をやめました - 読書メモ</title>
      <link>/posts/2026-01-09-reading-working/</link>
      <pubDate>Fri, 09 Jan 2026 19:30:00 +0900</pubDate>
      <guid>/posts/2026-01-09-reading-working/</guid>
      <description>&lt;h2 id=&#34;本の情報&#34;&gt;本の情報&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;タイトル&lt;/strong&gt;: 本が読めなかったから、仕事をやめました&lt;br&gt;
&lt;strong&gt;著者&lt;/strong&gt;: [著者名不明]&lt;br&gt;
&lt;strong&gt;読書期間&lt;/strong&gt;: 2025年1月9日&lt;br&gt;
&lt;strong&gt;読了状況&lt;/strong&gt;: 大正時代まで（未完）&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;まえがき---衝撃の一文&#34;&gt;まえがき - 衝撃の一文&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本が読めなかったから、仕事をやめました★&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;ロックすぎる。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;著者の状況&#34;&gt;著者の状況&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;読書好き、本を買うために働く&lt;/li&gt;
&lt;li&gt;週5勤務、21時まで残業&lt;/li&gt;
&lt;li&gt;気づいたら1年間、本を読んでいない&lt;/li&gt;
&lt;li&gt;時間があってもスマホを見てしまう&lt;/li&gt;
&lt;li&gt;本を開いても目が閉じる、YouTubeに逃げる&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3年半後、退職&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;退職後、ゆっくり読書できるようになった&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;著者の問題提起&#34;&gt;著者の問題提起&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;会社で働きながら本を読むことは難しい&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;本を読む余裕のない社会はおかしい&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;→ SNSで多くの同意が集まる&lt;br&gt;
→ 趣味全般を続けづらい社会への問い&lt;br&gt;
→ &lt;strong&gt;「あなたの文化は労働に搾取されている」&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;所感-共感と違和感&#34;&gt;所感: 共感と違和感&lt;/h2&gt;
&lt;h3 id=&#34;共感ポイント&#34;&gt;共感ポイント&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;仕事のために生きている人が多数派でビビる&lt;/li&gt;
&lt;li&gt;仕事は微妙、人間関係は辛い&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;これ、私のことだ&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;読書好きでもこうなるのか&amp;hellip;（本当なら）&lt;/li&gt;
&lt;li&gt;余裕がない社会&lt;/li&gt;
&lt;li&gt;働きながらX（Twitter）をやるのはマジで辛い&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;違和感&#34;&gt;違和感&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;突然「搾取」という言葉が出てきて怖い&lt;/li&gt;
&lt;li&gt;自称漫画家、バンドマンのようなゴミは働きながら続けるべき
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;辛いからこそ、続けるってことは熱量があるってこと&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第1章-労働と文化的生活の両立&#34;&gt;第1章: 労働と文化的生活の両立&lt;/h2&gt;
&lt;h3 id=&#34;著者の姿勢&#34;&gt;著者の姿勢&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;文句だけ言っても仕方ない&lt;/li&gt;
&lt;li&gt;歴史から学ぶアプローチ&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;なぜ今、両立しなくなったのか&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;どうしたら両立できるのか&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;花束みたいな恋をした分析&#34;&gt;『花束みたいな恋をした』分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;登場人物:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;麦: 地方の花火職人 → 会社員&lt;/li&gt;
&lt;li&gt;絹: 金持ち、大企業&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;展開:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就職後、麦は忙しくなる&lt;/li&gt;
&lt;li&gt;漫画が続かない、頭に入らない&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;パズドラしかやる気しない&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;絹からの本も無視&lt;/li&gt;
&lt;li&gt;心が離れていく&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;テーマ:&lt;/strong&gt;
長時間労働と文化的生活は両立しないという前提の作品&lt;/p&gt;
&lt;h3 id=&#34;速読自己啓発ブームの意味&#34;&gt;速読・自己啓発ブームの意味&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Amazonで速読、情報処理スキル、読書術が人気&lt;/li&gt;
&lt;li&gt;趣味ではなく、自己啓発メイン&lt;/li&gt;
&lt;li&gt;効率優先&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;→ 労働と読書の両立をみんななんとかしようとした結果&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;ファスト教養も同じ構造&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第2章-格差と読書&#34;&gt;第2章: 格差と読書&lt;/h2&gt;
&lt;h3 id=&#34;階級格差が読書意欲に影響&#34;&gt;階級格差が読書意欲に影響&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;麦（労働者）vs 絹（富裕層）の対比&lt;/li&gt;
&lt;li&gt;働けど働けど暮らしは楽にならず&lt;/li&gt;
&lt;li&gt;本を読む余裕さえなくなる&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;暮らしの格差が余暇の時間も奪う&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;独学大全の指摘&#34;&gt;『独学大全』の指摘&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;格差は動機づけの段階から現れる&lt;/li&gt;
&lt;li&gt;学ぶ動機づけがない者 → 学問は役に立たない、僻む&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;意欲から格差が生まれる&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第3章-明治時代---長時間労働と読書の始まり&#34;&gt;第3章: 明治時代 - 長時間労働と読書の始まり&lt;/h2&gt;
&lt;h3 id=&#34;労働環境&#34;&gt;労働環境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;この頃から長時間労働&lt;/li&gt;
&lt;li&gt;工場労働者: 農民時代より断然長時間&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平均残業時間: 2時間&lt;/strong&gt; &amp;hellip;あれ？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;化学工場: 12時間労働&lt;/strong&gt; &amp;hellip;は？&lt;/li&gt;
&lt;li&gt;労働組合はゴミ、割増料金が魅力的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;→ 今もだいたい同じ&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;明治時代の感覚&#34;&gt;明治時代の感覚&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;「最近はみんな忙しそうにしてる」&lt;/li&gt;
&lt;li&gt;余裕がなくなった感じ、せっかち&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;近代化 = せっかち&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;読書革命&#34;&gt;読書革命&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;句読点と黙読の発明:&lt;/strong&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>DFS/BFSの本質：深さと幅を支配するデータ構造の選択</title>
      <link>/posts/2026-01-09-dfs-bfs/</link>
      <pubDate>Fri, 09 Jan 2026 06:30:00 +0900</pubDate>
      <guid>/posts/2026-01-09-dfs-bfs/</guid>
      <description>&lt;p&gt;DFS, BFSがわかりづらかったので、いくつかの記事を見て個人的に感じた疑問や
「こういうコード例が欲しい」という要望を踏まえて生成AIに生成してもらった。&lt;/p&gt;
&lt;p&gt;生成された内容を検証し、コードを実際に動かして確認したところ、
自分の理解が深まる良い記事になったので、このまま公開することにした。&lt;/p&gt;
&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;LeetCodeでMedium問題を解いていると、必ず遭遇するのがDFS（深さ優先探索）とBFS（幅優先探索）だ。&lt;/p&gt;
&lt;p&gt;「Dは深さ、Bは幅」というのは知っている。でも、&lt;strong&gt;なぜスタックとキューを使い分けるのか&lt;/strong&gt;？その本質を理解している人は意外と少ない。&lt;/p&gt;
&lt;p&gt;今回は、入れ子リストの例を使って、DFS/BFSの動作原理とデータ構造の関係を視覚的に解説する。&lt;/p&gt;
&lt;h2 id=&#34;問題設定入れ子リストをフラット化する&#34;&gt;問題設定：入れ子リストをフラット化する&lt;/h2&gt;
&lt;p&gt;以下のような入れ子構造のリストがあるとする。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, [&lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;, [&lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;7&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;8&lt;/span&gt;], &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;], &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;これをフラットな配列にしたい。このとき、「どの順番で要素を取り出すか」がDFS/BFSの違いだ。&lt;/p&gt;
&lt;h2 id=&#34;ツリー構造として可視化する&#34;&gt;ツリー構造として可視化する&lt;/h2&gt;
&lt;p&gt;入れ子リストは、実はツリー構造として表現できる。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;        root
       / | \
      1  []  3
         |
        /|\ \
       4 5 [] 2
           |
          /|\
         6 7 8
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;この木をどう巡回するかで、DFSとBFSが決まる。&lt;/p&gt;
&lt;h2 id=&#34;dfs深さ優先探索とにかく深く潜る&#34;&gt;DFS（深さ優先探索）：とにかく深く潜る&lt;/h2&gt;
&lt;h3 id=&#34;動作イメージ&#34;&gt;動作イメージ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;「見つけた枝があれば、まずそこを最後まで探索する」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;訪問順序：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;1 → [中に入る] → 4 → 5 → [さらに中] → 6 → 7 → 8 
→ [戻る] → 2 → [戻る] → 3
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;結果：&lt;code&gt;[1, 4, 5, 6, 7, 8, 2, 3]&lt;/code&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Audibleで学ぶヨーロッパ中世史：雑談メモ</title>
      <link>/posts/2026-01-03-history_europe_1/</link>
      <pubDate>Sat, 03 Jan 2026 22:00:00 +0900</pubDate>
      <guid>/posts/2026-01-03-history_europe_1/</guid>
      <description>Audibleでヨーロッパ史を聞きながら取ったメモと雑談。ビザンツ帝国、カロリング朝、そして歴史上の人物への率直な感想。</description>
    </item>
    <item>
      <title>k3s &#43; Drone CI/CD構築体験記② 手動ビルドでなんとか動いた</title>
      <link>/posts/2026-01-01-hugo-proxmox-drone-2/</link>
      <pubDate>Thu, 01 Jan 2026 18:00:00 +0900</pubDate>
      <guid>/posts/2026-01-01-hugo-proxmox-drone-2/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://mintblog.hatenablog.com/entry/2026/01/01/112426&#34;&gt;前回のハマり話&lt;/a&gt;の続編。&lt;/p&gt;
&lt;p&gt;今回は実際にCI/CDパイプラインを動かすところまで進めた。結論から言うと、自動化は99%完成したが、最後の1%（Webhook）で詰んだ。&lt;/p&gt;
&lt;h2 id=&#34;目標設定&#34;&gt;目標設定&lt;/h2&gt;
&lt;p&gt;理想は当然これ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub push → Drone検知 → Hugo自動ビルド → Dockerイメージ作成 → k3sデプロイ更新&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ただし、私の環境には致命的な制約がある。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;制約：外部IP持ってない&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;自宅サーバーはTailscaleでVPN経由でのみアクセス可能。つまりGitHubからのWebhookが届かない。まあ、DuckDNSでドメインは取ってるけど、それでもTailscale依存の構成。&lt;/p&gt;
&lt;p&gt;それでも「やれるとこまでやってみよう」精神で進めた。&lt;/p&gt;
&lt;h2 id=&#34;droneyml-設定&#34;&gt;.drone.yml 設定&lt;/h2&gt;
&lt;p&gt;最終的にはこんな感じになった：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;pipeline&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;type&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;kubernetes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;hugo-pipeline&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;build-hugo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;klakegg/hugo:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;commands&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#ae81ff&#34;&gt;cd posts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#ae81ff&#34;&gt;hugo --minify&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#ae81ff&#34;&gt;ls -la public/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;create-docker-context&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;alpine:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;commands&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#ae81ff&#34;&gt;cp -r posts/public ./public&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#ae81ff&#34;&gt;ls -la public/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;docker-build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;plugins/docker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;settings&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;registry&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ghcr.io&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;repo&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ghcr.io/wasuken/tech_blog&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;username&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;from_secret&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;github_username&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;password&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;from_secret&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;github_token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;tags&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;${DRONE_COMMIT_SHA:0:8}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;deploy-to-k3s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bitnami/kubectl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;environment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;KUBECONFIG&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;from_secret&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;kubeconfig&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;commands&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;kubectl set image deployment/hugo-site hugo=ghcr.io/wasuken/tech_blog:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;kubectl rollout status deployment/hugo-site&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;deploy-complete&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;alpine:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;commands&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#ae81ff&#34;&gt;echo &amp;#34;Hugo build complete!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#ae81ff&#34;&gt;echo &amp;#34;Image pushed successfully&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ポイントは、HugoビルドからDockerイメージ作成、GHCR（GitHub Container Registry）へのプッシュ、最終的なk3sデプロイまで全部自動化したこと。&lt;/p&gt;</description>
    </item>
    <item>
      <title>ローカル環境を汚さない静的サイト構築 - Hugo Docker Compose環境構築記録</title>
      <link>/posts/2025-12-21-hugo-blog-setup/</link>
      <pubDate>Sun, 21 Dec 2025 23:00:00 +0900</pubDate>
      <guid>/posts/2025-12-21-hugo-blog-setup/</guid>
      <description>&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;
&lt;p&gt;日課でできる範囲の活動として、軽い記事から、疑問を生成AIに出してもらって、それに答えてもらって、深堀や補足、添削をしてもらった内容までを記事にするという習慣を続けていたが、公開するのはどうなのかなと思った。&lt;/p&gt;
&lt;p&gt;しかし、後ほど止めるのはもったいないということで妥協案として、ローカルで動くブログには投稿することにした。&lt;/p&gt;
&lt;p&gt;なので、ローカルブログを立ち上げることにした。&lt;/p&gt;
&lt;p&gt;最初はGitHub Pagesでよく使われているJekyllを試した。しかし、ローカル環境とDocker環境でRubyのバージョン不一致が発生し、プロジェクト初期化の段階で躓いた。&lt;/p&gt;
&lt;p&gt;ローカルのRuby 3.4に対してDockerの最新イメージがRuby 3.1で、この差分が原因でSCSS変換周りでエラーが頻発。Jekyllはプロジェクト作成をローカルで行う必要があるため、「Docker使えば環境差を吸収できる」という謳い文句が実質的に機能しなかった。&lt;/p&gt;
&lt;p&gt;もっとうまくやればよかっただろうが、そのときは血が登っていて、Hugoにしてしまった。&lt;/p&gt;
&lt;h2 id=&#34;要件整理&#34;&gt;要件整理&lt;/h2&gt;
&lt;p&gt;改めて自分の要件を整理した：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Markdownファイルのマウントだけで完結&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ローカル環境に一切依存しない&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;プロジェクト初期化もDocker内で実行可能&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;検索機能とファイル一覧が欲しい&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;これを満たすツールを探した結果、Hugoに行き着いた。&lt;/p&gt;
&lt;h2 id=&#34;なぜhugoなのか&#34;&gt;なぜHugoなのか&lt;/h2&gt;
&lt;p&gt;Hugoを選んだ理由は明確：&lt;/p&gt;
&lt;h3 id=&#34;1-バイナリ単体で動作&#34;&gt;1. バイナリ単体で動作&lt;/h3&gt;
&lt;p&gt;Go言語で書かれたHugoは単一バイナリで動作する。RubyやNode.js、Pythonのようなランタイム環境が不要。これにより依存関係地獄から解放される。&lt;/p&gt;
&lt;h3 id=&#34;2-プロジェクト初期化もdocker内で完結&#34;&gt;2. プロジェクト初期化もDocker内で完結&lt;/h3&gt;
&lt;p&gt;当初は生成AIの言うとおりに以下のコマンドでプロジェクトを作成した。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker run --rm -v &lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;pwd&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;/posts:/src klakegg/hugo:alpine new site .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;この1コマンドでプロジェクト作成が完了する。ローカルに何もインストールする必要がない。&lt;/p&gt;
&lt;p&gt;のだが、後ほどこれがトラブルを産んだ。&lt;/p&gt;
&lt;h3 id=&#34;3-高速なビルド&#34;&gt;3. 高速なビルド&lt;/h3&gt;
&lt;p&gt;Goの並列処理能力により、数千ページ規模のサイトでも秒単位でビルドが完了する。開発時のホットリロードも快適。&lt;/p&gt;
&lt;h2 id=&#34;構築手順&#34;&gt;構築手順&lt;/h2&gt;
&lt;h3 id=&#34;1-docker-composeyml作成&#34;&gt;1. docker-compose.yml作成&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;hugo&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;hugomods/hugo:base&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;container_name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;hugo-blog&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;7000:7000&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ae81ff&#34;&gt;./posts:/src&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;command&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;server --bind 0.0.0.0 --port 7000 --buildDrafts --buildFuture&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;restart&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;unless-stopped&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ポイント：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hugomods/hugo:base&lt;/code&gt; を使用&lt;/li&gt;
&lt;li&gt;ポートは7000にマッピング（後述のブラウザ制限回避）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--buildDrafts --buildFuture&lt;/code&gt; で下書きと未来日付の記事も表示&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;2-プロジェクト初期化&#34;&gt;2. プロジェクト初期化&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker run --rm -v &lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;pwd&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;/posts:/src klakegg/hugo:alpine new site .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;これで &lt;code&gt;posts/&lt;/code&gt; ディレクトリに必要なファイル群が生成される。&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQLのpg_trgmで中間一致検索を高速化する仕組みを学ぶ</title>
      <link>/posts/2025-12-21-pgsql-pg-trigm/</link>
      <pubDate>Sun, 21 Dec 2025 12:00:00 +0900</pubDate>
      <guid>/posts/2025-12-21-pgsql-pg-trigm/</guid>
      <description>&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;p&gt;この記事は、以下の記事を読んで疑問に思ったことを調べた学習記録である。&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://zenn.dev/team_zenn/articles/zenn-search-tuning-story&#34;&gt;Zennの検索スピードを5倍に高速化した話&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;記事では、Zennのサイト内検索をpg_trgm拡張を使って平均6倍、95パーセンタイルで4.25倍高速化した事例が紹介されている。&lt;/p&gt;
&lt;h2 id=&#34;なぜ中間一致検索は遅いのか&#34;&gt;なぜ中間一致検索は遅いのか&lt;/h2&gt;
&lt;p&gt;通常、PostgreSQLで&lt;code&gt;LIKE &#39;%keyword%&#39;&lt;/code&gt;のような中間一致検索を実行すると、BTreeインデックスが使えずフルスキャンが発生する。BTreeインデックスは文字列の前方一致には有効だが、中間一致では活用できない構造になっているためである。&lt;/p&gt;
&lt;p&gt;データ量が増えると、このフルスキャンが深刻なパフォーマンスボトルネックになる。参考記事では、検索に1秒〜数秒かかる状態だったとのことだ。&lt;/p&gt;
&lt;h2 id=&#34;n-gramインデックスの仕組み&#34;&gt;n-gramインデックスの仕組み&lt;/h2&gt;
&lt;p&gt;n-gramインデックスは、文字列をn文字ずつに分割してインデックス化することで、中間一致検索でもインデックスを効かせる仕組みである。&lt;/p&gt;
&lt;h3 id=&#34;3-gramの例&#34;&gt;3-gramの例&lt;/h3&gt;
&lt;p&gt;「PostgreSQL」という文字列を3-gram（トライグラム）で分割すると以下のようになる。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;__P, _Po, Pos, ost, stg, tgr, gre, reS, eSQL, QL_, L__
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;先頭と末尾にはパディング文字（&lt;code&gt;_&lt;/code&gt;）が付与される。&lt;/p&gt;
&lt;h3 id=&#34;検索時の動作&#34;&gt;検索時の動作&lt;/h3&gt;
&lt;p&gt;「stgre」というキーワードで検索する場合：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;検索キーワードを3-gramで分割: &lt;code&gt;stg&lt;/code&gt;, &lt;code&gt;tgr&lt;/code&gt;, &lt;code&gt;gre&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;インデックスから&lt;strong&gt;これらすべてのトライグラムを含む&lt;/strong&gt;文書を抽出&lt;/li&gt;
&lt;li&gt;抽出された候補に対してRecheck処理を実行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;重要なのは「いずれか」ではなく「&lt;strong&gt;すべて&lt;/strong&gt;」のトライグラムが存在する文書が候補になる点である。もし「いずれか」だと、無関係な文書が大量に候補に含まれてしまう。&lt;/p&gt;
&lt;h2 id=&#34;recheck処理が必要な理由&#34;&gt;Recheck処理が必要な理由&lt;/h2&gt;
&lt;p&gt;n-gramインデックスでは、インデックスレベルでの検索後に必ずRecheck処理が必要になる。&lt;/p&gt;
&lt;h3 id=&#34;具体例&#34;&gt;具体例&lt;/h3&gt;
&lt;p&gt;以下のような状況を考える。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本文: 「小学校校長」&lt;/li&gt;
&lt;li&gt;クエリ: 「小学校長」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3-gramで分割すると：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「小学校校長」→ &lt;code&gt;小学校&lt;/code&gt;, &lt;code&gt;学校校&lt;/code&gt;, &lt;code&gt;校校長&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;「小学校長」→ &lt;code&gt;小学校&lt;/code&gt;, &lt;code&gt;学校長&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;「小学校」が共通しているため、n-gramレベルでは「小学校校長」が候補として抽出される。しかし実際には「小学校長」という文字列は含まれていない。&lt;/p&gt;
&lt;p&gt;このような&lt;strong&gt;false positive（誤検出）を除外するため&lt;/strong&gt;、インデックスで絞り込んだ候補に対して、実際に検索キーワードが含まれているかを厳密にチェックする必要がある。これがRecheck処理である。&lt;/p&gt;
&lt;h2 id=&#34;pg_trgmとpg_bigmの選択&#34;&gt;pg_trgmとpg_bigmの選択&lt;/h2&gt;
&lt;p&gt;PostgreSQLには2つの主要なn-gram拡張がある。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pg_trgm&lt;/strong&gt;: 3-gram方式、PostgreSQL本体にcontribとして付属&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pg_bigm&lt;/strong&gt;: 2-gram方式、サードパーティ製（NECが開発）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;比較表&#34;&gt;比較表&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;機能&lt;/th&gt;
          &lt;th&gt;pg_trgm&lt;/th&gt;
          &lt;th&gt;pg_bigm&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;エコシステム&lt;/td&gt;
          &lt;td&gt;PostgreSQLコミュニティ&lt;/td&gt;
          &lt;td&gt;サードパーティ&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;ILIKE対応&lt;/td&gt;
          &lt;td&gt;○&lt;/td&gt;
          &lt;td&gt;×&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;2文字以下の検索&lt;/td&gt;
          &lt;td&gt;×&lt;/td&gt;
          &lt;td&gt;○&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Recheck無効化&lt;/td&gt;
          &lt;td&gt;×&lt;/td&gt;
          &lt;td&gt;○&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;インデックスサイズ&lt;/td&gt;
          &lt;td&gt;小&lt;/td&gt;
          &lt;td&gt;大（約2倍）&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;なぜpg_trgmが選ばれたか&#34;&gt;なぜpg_trgmが選ばれたか&lt;/h3&gt;
&lt;p&gt;参考記事では、以下の理由でpg_trgmのみを採用している。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
