テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その2

前回のおさらい その1では ValueObject・Policy・Entity を作った。ポイントは「フレームワークを一切使わないので、テストがただの関数呼び出しになる」という体験だった。 今回はいよいよ Service 層に入る。ここが設計の核心だ。 複数のクラスをまたぐフローを、DI(依存性の注入)を使って DB から切り離す。 今回追加したもの src/ domain/ entity/ User.ts # 追加 Review.ts # 追加 valueobject/ UserName.ts # 追加 ReviewComment.ts # 追加 repository/ BookRepository.ts # 追加(Interface) service/ BookShelfService.ts # 追加 User と Review を追加する まず Entity の準備。今回は単純なので ValueObject から作る。 UserName export class UserName { constructor(public readonly value: string) { if (!value || value.trim().length === 0) { throw new Error("UserNameは空にできない"); } if (value.length > 50) { throw new Error("UserNameは50文字以内"); } } } trim() してから空チェックしているのがポイント。スペースだけのユーザー名を弾く。 ReviewComment export class ReviewComment { constructor(public readonly value: string) { if (!value || value.trim().length === 0) { throw new Error("ReviewCommentは空にできない"); } if (value.length > 1000) { throw new Error("ReviewCommentは1000文字以内"); } } } User Entity import { UserName } from "../valueobject/UserName"; export class User { constructor( public readonly id: string, public readonly name: UserName, ) {} } User 自体はシンプルだ。ロジックがない。ビジネスルールが増えれば changeXxx() メソッドが生える設計だが、今は id と name を持つだけでいい。 ...

March 4, 2026 · 4 min

テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その3

はじめに Part1 でValueObject・Policy・Entityを作り、Part2 でServiceをDI+Mockテストで固めた。 累計13テスト、全パスの状態だ。 Part3ではいよいよDBを繋ぐ。やることは3つ。 Prismaセットアップ+スキーマ定義 PrismaBookRepository 実装(ドメインオブジェクトへの変換) Route HandlerでDIを組み立てる そして最後に「テストを書かない層を意図的に決める」という話をする。 1. Prismaセットアップ npm install prisma @prisma/client npx prisma init --datasource-provider sqlite prisma/schema.prisma に Bookモデルを定義する。 generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Book { id String @id title String isbn String status String rating Int? } User と Review はまだDBに持たない。Book の CRUD が動けば Part3 のゴール。 なぜ status を String で持つのか Prismaは SQLiteで enum をネイティブサポートしていない。 そのため status String で持ち、取り出し時に as ReadingStatus でキャストする。 ...

March 4, 2026 · 4 min

テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その4

はじめに Part1 でValueObject・Policy・Entityを作り、Part2 でServiceをDI+Mockテストで固めた。 Part3 でPrismaを繋ぎ、GET /api/books と POST /api/books を動かした。 Part4では残りのエンドポイントを実装する。やることは3つ。 カスタム例外クラスの導入 PATCH /api/books/:id/start と PATCH /api/books/:id/complete の実装 DELETE /api/books/:id の実装 そして「エラー種別ごとにHTTPステータスを整理する」という設計判断を掘り下げる。 また、Part3でPrisma v7特有のセットアップをしたが、v6以前と何が変わったのかをここで整理しておく。 0. Prisma v7で何が変わったか Part3でPrismaをセットアップしたとき、v6以前と比べていくつか「見慣れない書き方」が必要だった。 v7は破壊的変更が多く、ネット上のv6時代の記事を参考にすると詰まる箇所がある。 ここで整理しておく。 参考: Upgrade to Prisma ORM 7 | Prisma Documentation generator の変更 // ❌ v6以前 generator client { provider = "prisma-client-js" } // ✅ v7 generator client { provider = "prisma-client" output = "../src/generated/prisma" } v7では prisma-client-js が廃止され prisma-client に変わった。 Rustベースのエンジンを廃止しTypeScriptネイティブになったことに伴う変更だ。 また output が必須になり、node_modules への自動生成はなくなった。 ...

March 4, 2026 · 6 min

テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その5

はじめに Part4 でカスタム例外クラスと残りのエンドポイントを実装し、APIが完成した。 Part5では src/app/page.tsx に簡易UIを実装する。バックエンドのロジックやテストには一切触れない。 フロントエンドからAPIを叩いて動くものを作るだけだ。 実装方針 page.tsx をClient Componentにする。 Server Componentでfetchする方法もあるが、ボタン操作のたびにstateを更新して再描画する必要がある。 今回のUIは「ボタンを押す → APIを叩く → 一覧を再取得して画面を更新する」という流れが中心なので、 useState + useEffect で管理するClient Componentのほうがシンプルだ。 "use client"; ファイル先頭にこの1行を追加する。 実装するUI 本の追加フォーム(id / title / isbn) 本棚(Unread / Reading / Completed のグループ表示) 各本へのアクションボタン Unread → 「読み始める」ボタン Reading → 評価入力(1〜5)+「読了にする」ボタン 全ステータス → 削除ボタン 型定義 APIレスポンスの型を定義する。 type BookStatus = "Unread" | "Reading" | "Completed"; type Book = { id: string; title: string; isbn: string; status: BookStatus; rating: number | null; }; バックエンドの ReadingStatus は as const で定義した文字列リテラルなので、そのまま使える。 データ取得 async function fetchBooks() { try { const res = await fetch("/api/books"); if (!res.ok) throw new Error("取得失敗"); const data: Book[] = await res.json(); setBooks(data); } catch (e) { setError(e instanceof Error ? e.message : "不明なエラー"); } finally { setLoading(false); } } useEffect(() => { fetchBooks(); }, []); fetchBooks は追加・ステータス変更・削除の後にも呼ぶ。 サーバーの状態を正として再取得するシンプルな方針だ。 楽観的更新(Optimistic Update)は今回やらない。 ...

March 4, 2026 · 2 min

AVL木と赤黒木をPythonで実装して比較する

はじめに 動かしながらゼロから学ぶLinuxカーネルの教科書 第2版 上記の技術書を読んでいてLinuxスケジューラの話が出てきた。CFSもEEVDFも赤黒木を使っている。赤黒木とは何かを調べていくうちにAVL木との比較が面白かったので、両方Pythonで実装してベンチマークを取った。 実装コードはClaude (Anthropic) により生成。リポジトリは以下。 https://github.com/wasuken/avl-rb-tree 自己平衡二分探索木とは まず前提として、ただの二分探索木には問題がある。 挿入順: 1, 2, 3, 4, 5 1 \ 2 \ 3 \ 4 ← 一直線になる 挿入順によっては木が一直線になり、検索がO(n)に劣化する。これを解決するのが自己平衡二分探索木で、挿入・削除のたびに自動でバランスを取り直す。AVL木と赤黒木はどちらもこのカテゴリに属する。 AVL木 1962年にソ連の数学者Adelson-VelskyとLandis(AVLの名前の由来)が考案した世界初の自己平衡二分探索木。 バランスの管理方法 各ノードに高さ(height)を持たせ、左右の高さの差(バランス係数)が常に1以内になるよう管理する。 def _balance_factor(self, node): return self._height(node.left) - self._height(node.right) 差が2以上になったら回転で修正する。 回転 回転は「親子関係を1段入れ替えるだけ」の操作。二分探索木の順序を壊さずに形だけ変える。 rotate_right(y): y x / \ / \ x C → A y / \ / \ A t2 t2 C t2 は回転で行き場を失う孫ノード。二分探索木の順序的に x < t2 < y が保証されているので、yの左に付け替えるだけでよい。 バランス崩れのパターンは4つ(LL, RR, LR, RL)で、それぞれ1〜2回の回転で解消できる。 挿入・削除 挿入・削除のたびに _rebalance が呼ばれ、高さの更新とバランスチェックが走る。 ...

March 1, 2026 · 2 min

LinuxのCFSとEEVDFを整理する - スケジューラはなぜ赤黒木を使うのか

はじめに 動かしながらゼロから学ぶLinuxカーネルの教科書 第2版 上記の技術書を読んでいてスケジューラ周りの理解が曖昧だったので、生成AIや公式ドキュメントを使って整理した。 CFS (Completely Fair Scheduler) とは Linux 2.6.23から導入されたプロセススケジューラ。「全プロセスに公平にCPU時間を与える」という思想で設計されている。 vruntime(仮想実行時間) vruntimeは「実際の実行時間をNICE値で補正した値」で、CFSの核心となる指標。 vruntime += 実際のCPU時間 × (1024 / プロセスの重み) NICE値が低い(優先度高)→ 重みが大きい → vruntimeの増加が遅い → より長くCPUを使える NICE値が高い(優先度低)→ 重みが小さい → vruntimeの増加が速い → すぐ交代させられる CFSは「vruntimeが最も小さいプロセスを次に実行する」というルールで動く。後ろ向きの指標(過去の使用量の累積)であることがEEVDFとの本質的な差になる。 NICE値と重み NICE値は -20(最高優先度)〜 +19(最低優先度)の範囲で、内部的に重みに変換される。 NICE 0 → weight 1024 NICE -1 → weight 1277(約1.25倍) NICE +1 → weight 820(約0.8倍) NICE -20 → weight 88761 NICE +19 → weight 15 1段階変わるごとに約10%のCPU時間が変化する設計になっている。 タイムスライスとスケジューリングレイテンシ スケジューリングレイテンシは「全プロセスが最低1回実行されるべき目標周期」。デフォルト約6〜24ms(プロセス数による)。 タイムスライスはその比例配分: タイムスライス = スケジューリングレイテンシ × (タスクの重み / キュー内の全タスクの重みの合計) 具体例: ...

March 1, 2026 · 2 min

Linuxの起動フローを整理する - UEFI/BIOSからinitまで

はじめに [https://info.nikkeibp.co.jp/media/LIN/atcl/books/070900046/:embed:cite] 上記の技術書を読んでいて、ブートローダとLinuxの初期スタート時の役割とか順番がいまいち掴めなかったので生成AIや他の記事など別軸から調べ直してまとめた。 起動フロー全体像 UEFI/BIOS ↓ POST(ハードウェア初期化)、ブートデバイス選択 ブートローダー(GRUB等) ↓ /boot/vmlinuz(カーネルイメージ)をメモリに展開 ↓ /boot/initramfs をメモリに展開 カーネル起動 ↓ initramfsを一時的な / としてマウント ↓ ドライバ読み込み、本物のrootデバイスを認識 ↓ 本物のroot FSをマウント(switch_root) /sbin/init(systemd)に移譲 各フェーズの詳細 1. UEFI/BIOS 起動の最初はUEFI(または旧来のBIOS)が担う。 POST(Power-On Self Test): メモリ、CPU、周辺デバイスの初期化 ブートデバイスの選択(NVMe, SSD, PXEなど) UEFIの場合はEFIパーティション(ESP)から .efi ファイルを直接実行できる UEFIとBIOSの大きな違いとして、UEFIはGPTディスクのネイティブサポートや、セキュアブートの仕組みを持つ。 2. ブートローダー(GRUB2等) UEFI/BIOSからブートローダーに制御が渡る。 代表的なものはGRUB2で、設定ファイルは /boot/grub/grub.cfg にある。 ブートローダーの役割はシンプルで、以下の2点だけ: カーネルイメージ(vmlinuz)をメモリに展開する initramfs(initramfs-*.img)をメモリに展開する # /boot 以下の典型的な構成 $ ls /boot/ grub/ initramfs-6.1.0-28-amd64.img vmlinuz-6.1.0-28-amd64 ブートローダー自身はルートFSのマウントをしない。あくまでカーネルとinitramfsをメモリに置いて制御を渡すだけ。 3. カーネル起動とinitramfs ここが一番誤解されやすいフェーズ。 カーネルが起動すると、まず**initramfs(Initial RAM Filesystem)**を一時的なルート(/)としてマウントする。 なぜinitramfsが必要か? カーネル本体はコンパクトに保つ設計になっており、NVMeやLVMやLUKS(暗号化)といった本物のディスクにアクセスするためのドライバを、起動時に動的にロードする必要がある。 initramfsはそのためのミニマルな環境を提供する。 initramfs の中身(概略) /init → 起動スクリプト /lib/modules → カーネルモジュール(ドライバ) /bin, /sbin → busybox等の最低限のコマンド群 処理の流れ: ...

February 28, 2026 · 1 min

歴史地図アプリを雑にk3sへデプロイした

歴史地図アプリの構成 React + TypeScript + Vite + MapLibre GL のSPA。歴史的国境データ(GeoJSON)を表示するアプリ。 データはpublic/data/以下にGeoJSONを置く構成で、.gitignoreに含めているためリポジトリには入っていない。 ちなみに、生成するスクリプトはあるが、GEMINIを利用しないといけない。 しかし、APIキーのレート制限が入ってしまったので、ローカルで生成済みのデータを持ち込むことにした。 インフラ構成 自宅のProxmox上にLXCコンテナとしてk3sクラスタを構築している。マスター1台+ノード1台の最小構成。 外部公開はNginx Proxy Manager(NPM)でポートフォワーディングしており、DuckDNSのドメインにSSL終端している。 インターネット ↓ Nginx Proxy Manager(SSL終端) ↓ k3s NodePort ↓ Pod 問題:データファイルをどう持ち込むか public/data/がgitignoreされているため、コンテナ内でgit cloneしてもデータが存在しない。 選択肢はいくつかあったが、今回はk3sのhostPathボリュームでマウントする方針にした。 だるいファイル転送 データファイルをk3sノードに転送するのが一番面倒だった。 Proxmoxのファイルアップロード → UIの制限でNG ngrok経由 → Tailscale環境のためlocalhostの名前解決失敗 結局TailscaleのIPでProxmoxホストに転送 → pct pushでLXCコンテナへ # ProxmoxホストからLXCへ pct push <CTID> /path/to/data.tar.gz /tmp/data.tar.gz # k3sマスターで解凍 mkdir -p /opt/history-map-data tar -xzf /tmp/data.tar.gz -C /opt/history-map-data 融通の効かないViteとふわふわClaude君の罠 npm run previewはデフォルトで許可ホストを制限する。Nginx Proxy Manager経由でアクセスするとBlocked requestが出る。 環境変数で全許可とかできたらよかったけど、結論だけ言うとできなかった。少なくともClaude君の指示では何をどうしても駄目だったので、最終的にvite.config.tsをデプロイ時に動的に書き換えることで回避した。 cat > /app/vite.config.ts << 'EOF' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], preview: { allowedHosts: ['your-domain.example.com'], }, }) EOF 最終的なYAML apiVersion: apps/v1 kind: Deployment metadata: name: history-map spec: replicas: 1 selector: matchLabels: app: history-map template: metadata: labels: app: history-map spec: nodeName: k3s-master containers: - name: history-map image: node:20-alpine workingDir: /app command: ["sh", "-c"] args: - | apk add --no-cache git git clone https://github.com/wasuken/history-map-app.git /app --depth=1 cat > /app/vite.config.ts << 'EOF' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], preview: { allowedHosts: ['your-domain.example.com'], }, }) EOF mkdir -p /app/public/data cp -r /data/historical /app/public/data/historical cp -r /data/modern /app/public/data/modern cp /data/translation-cache.json /app/public/data/translation-cache.json npm install npx vite build npm run preview -- --host 0.0.0.0 --port 3000 ports: - containerPort: 3000 volumeMounts: - name: map-data mountPath: /data volumes: - name: map-data hostPath: path: /opt/history-map-data type: Directory --- apiVersion: v1 kind: Service metadata: name: history-map-service spec: selector: app: history-map ports: - port: 80 targetPort: 3000 nodePort: 30080 type: NodePort nodeName: k3s-masterを指定しているのはhostPathがPodの動くノード上に存在する必要があるため。ProxmoxのNPM(Nginx Proxy Manager)から このNodePortに向けてプロキシを設定している。 ...

February 25, 2026 · 2 min

WSL2でExpo + E2Eテスト(MaestroとDetox)を試みて完全に詰んだ話

TL;DR WSL2環境でExpo(React Native)のE2EテストをMaestroとDetoxで試みたが、どちらもWSL2とWindowsエミュレータの構造的な問題で動かなかった。 かなり過言ではあるが、あえて感情的になるならば、Mobile開発においてMac以外は人権がない。というかあまりにもMac環境以外がだるすぎる。 環境 OS: Windows + WSL2(Ubuntu) Expo SDK 54 / React Native 0.81.5 New Architecture有効 Androidエミュレータ: Windows側で動作(Medium Phone API 36) ADB: Windows側のものをWSL2から参照 Maestroを試みる インストール curl -Ls "https://get.maestro.mobile.dev" | bash export PATH="$HOME/.maestro/bin:$PATH" ここで最初の罠。maestro --helpを叩くとAI系の全く別のCLIツールが応答した。同名の別アプリが先にPATHに入っていたため。$HOME/.maestro/binをPATHの先頭に置くことで解決。 フローの準備 # .maestro/add_and_complete_task.yml appId: com.example.myapp --- - launchApp - tapOn: text: "追加" - inputText: "テストタスク" - tapOn: text: "追加する" - assertVisible: text: "NOW" 実行して即死 You have 0 devices connected, which is not enough to run 1 shards. エミュレータはWindows側で動いており、adb devicesにはemulator-5554が見えている。しかしMaestroはWSL2側でデバイスを探すため認識できない。 --udid=emulator-5554を指定しても: Device emulator-5554 was requested, but it is not connected. maestro start-device --platform=androidを試みると: ...

February 23, 2026 · 2 min

react-native-pdf 6.7.7のiOS表示問題をpatch-packageで解決する

はじめに 業務でreact-native-pdfを使用した際、AndroidではPDFが正常に表示されるのにiOSでは表示されないという問題に遭遇しました。 この記事では、GitHubのissueで共有された解決策であるpatch-packageを使ったパッチ適用方法について解説します。 問題の概要 環境 { "react-native-pdf": "^6.7.7", "react-native": "0.80.1", "react-native-blob-util": "^0.22.2" } 症状 Android: PDF表示が正常に動作 iOS: PDFが表示されない この問題は、React Native 0.80以降でreact-native-pdfを使用した際に発生することが確認されています。 参考: pdf is not displayed,Android is working fine, but there are problems with iOS #966 解決策: patch-packageを使う GitHubのissueで@anhnguyen123さんが共有してくれたパッチファイルを適用することで、この問題を解決できます。 1. patch-packageのインストール まず、patch-packageとpostinstall-postinstallをdevDependenciesとしてインストールします。 # npmの場合 npm install --save-dev patch-package # yarnの場合 yarn add --dev patch-package postinstall-postinstall 参考: patch-package - npm 2. package.jsonにpostinstallスクリプトを追加 package.jsonのscriptsセクションに、postinstallスクリプトを追加します。 { "scripts": { "postinstall": "patch-package" } } このスクリプトにより、npm installまたはyarn installを実行するたびに、自動的にパッチが適用されます。 3. パッチファイルの配置 GitHubのissueからパッチファイルreact-native-pdf+6.7.7.patchをダウンロードし、プロジェクトルートにpatchesディレクトリを作成してそこに配置します。 ...

February 17, 2026 · 2 min